From e4cfa7c86f793042c311e706ec1c70fcf86a1a9f Mon Sep 17 00:00:00 2001 From: OliBomby Date: Tue, 17 Sep 2024 11:27:23 +0200 Subject: [PATCH 01/23] Add view menu toggle for sample points --- osu.Game/Configuration/OsuConfigManager.cs | 4 +++- osu.Game/Localisation/EditorStrings.cs | 5 +++++ .../Components/Timeline/SamplePointPiece.cs | 15 ++++++++++++++- osu.Game/Screens/Edit/Editor.cs | 6 ++++++ 4 files changed, 28 insertions(+), 2 deletions(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 8d6c244b35..ae2025fb50 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -205,6 +205,7 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.EditorTimelineShowTimingChanges, true); SetDefault(OsuSetting.EditorTimelineShowTicks, true); + SetDefault(OsuSetting.EditorTimelineShowSamples, true); SetDefault(OsuSetting.AlwaysShowHoldForMenuButton, false); } @@ -431,6 +432,7 @@ namespace osu.Game.Configuration HideCountryFlags, EditorTimelineShowTimingChanges, EditorTimelineShowTicks, - AlwaysShowHoldForMenuButton + AlwaysShowHoldForMenuButton, + EditorTimelineShowSamples } } diff --git a/osu.Game/Localisation/EditorStrings.cs b/osu.Game/Localisation/EditorStrings.cs index bcffc18d4d..f72a0f4a26 100644 --- a/osu.Game/Localisation/EditorStrings.cs +++ b/osu.Game/Localisation/EditorStrings.cs @@ -139,6 +139,11 @@ namespace osu.Game.Localisation /// public static LocalisableString TimelineShowTicks => new TranslatableString(getKey(@"timeline_show_ticks"), @"Show ticks"); + /// + /// "Show samples" + /// + public static LocalisableString TimelineShowSamples => new TranslatableString(getKey(@"timeline_show_samples"), @"Show samples"); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index a8cf8723f2..3843d56d06 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -16,6 +16,7 @@ using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Framework.Utils; using osu.Game.Audio; +using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; @@ -40,6 +41,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline [Resolved] private Editor? editor { get; set; } + private Bindable samplesVisible = null!; + public SamplePointPiece(HitObject hitObject) { HitObject = hitObject; @@ -53,13 +56,23 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline protected virtual double GetTime() => HitObject is IHasRepeats r ? HitObject.StartTime + r.Duration / r.SpanCount() / 2 : HitObject.StartTime; [BackgroundDependencyLoader] - private void load() + private void load(OsuConfigManager config) { HitObject.DefaultsApplied += _ => updateText(); updateText(); if (editor != null) editor.ShowSampleEditPopoverRequested += onShowSampleEditPopoverRequested; + + samplesVisible = config.GetBindable(OsuSetting.EditorTimelineShowSamples); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + samplesVisible.BindValueChanged(visible => this.FadeTo(visible.NewValue ? 1 : 0, 200, Easing.OutQuint)); + this.FadeTo(samplesVisible.Value ? 1 : 0); } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 9bb91af806..b059557b28 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -215,6 +215,7 @@ namespace osu.Game.Screens.Edit private Bindable editorLimitedDistanceSnap; private Bindable editorTimelineShowTimingChanges; private Bindable editorTimelineShowTicks; + private Bindable editorTimelineShowSamples; /// /// This controls the opacity of components like the timelines, sidebars, etc. @@ -323,6 +324,7 @@ namespace osu.Game.Screens.Edit editorLimitedDistanceSnap = config.GetBindable(OsuSetting.EditorLimitedDistanceSnap); editorTimelineShowTimingChanges = config.GetBindable(OsuSetting.EditorTimelineShowTimingChanges); editorTimelineShowTicks = config.GetBindable(OsuSetting.EditorTimelineShowTicks); + editorTimelineShowSamples = config.GetBindable(OsuSetting.EditorTimelineShowSamples); AddInternal(new OsuContextMenuContainer { @@ -388,6 +390,10 @@ namespace osu.Game.Screens.Edit { State = { BindTarget = editorTimelineShowTicks } }, + new ToggleMenuItem(EditorStrings.TimelineShowSamples) + { + State = { BindTarget = editorTimelineShowSamples } + } ] }, new BackgroundDimMenuItem(editorBackgroundDim), From 34087a0f1a303d90bf443fd2d4b71e07049ee450 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Thu, 19 Sep 2024 16:20:26 +0200 Subject: [PATCH 02/23] Reduce sample point pieces to pink dot if zoomed out --- .../Timeline/HitObjectPointPiece.cs | 4 ++- .../Components/Timeline/SamplePointPiece.cs | 30 +++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/HitObjectPointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/HitObjectPointPiece.cs index 4b357d3a62..76323ac08c 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/HitObjectPointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/HitObjectPointPiece.cs @@ -17,6 +17,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { protected OsuSpriteText Label { get; private set; } + protected Container LabelContainer { get; private set; } + [BackgroundDependencyLoader] private void load(OsuColour colours) { @@ -26,7 +28,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline InternalChildren = new Drawable[] { - new Container + LabelContainer = new Container { AutoSizeAxes = Axes.X, Height = 16, diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index 3843d56d06..1638700735 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -41,6 +41,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline [Resolved] private Editor? editor { get; set; } + [Resolved] + private Timeline? timeline { get; set; } + private Bindable samplesVisible = null!; public SamplePointPiece(HitObject hitObject) @@ -59,6 +62,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private void load(OsuConfigManager config) { HitObject.DefaultsApplied += _ => updateText(); + Label.AllowMultiline = false; + LabelContainer.AutoSizeAxes = Axes.None; updateText(); if (editor != null) @@ -75,6 +80,30 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline this.FadeTo(samplesVisible.Value ? 1 : 0); } + private float lastZoom; + private const float zoom_threshold = 40f; + + protected override void Update() + { + base.Update(); + + // Retract visual state if the timeline is zoomed out too far. + if (timeline is null || timeline.Zoom == lastZoom) return; + + lastZoom = timeline.Zoom; + + if (timeline.Zoom < zoom_threshold) + { + Label.FadeOut(200, Easing.OutQuint); + LabelContainer.ResizeWidthTo(16, 200, Easing.OutQuint); + } + else + { + Label.FadeIn(200, Easing.OutQuint); + LabelContainer.ResizeWidthTo(Label.Width, 200, Easing.OutQuint); + } + } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); @@ -100,6 +129,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private void updateText() { Label.Text = $"{abbreviateBank(GetBankValue(GetSamples()))} {GetVolumeValue(GetSamples())}"; + LabelContainer.ResizeWidthTo(Label.Width, 200, Easing.OutQuint); } private static string? abbreviateBank(string? bank) From ce41dc46293875b9cb6ac9cd132c0e59c694c69c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 27 Sep 2024 17:13:26 +0900 Subject: [PATCH 03/23] Use bindable flow for zoom handling --- .../Edit/ManiaHitObjectComposer.cs | 2 +- .../Components/Timeline/SamplePointPiece.cs | 39 ++++++++----------- .../Timeline/ZoomableScrollContainer.cs | 15 +++---- 3 files changed, 26 insertions(+), 30 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs index 02a4f3a022..4a8f4aa279 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs @@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.Mania.Edit base.Update(); if (screenWithTimeline?.TimelineArea.Timeline != null) - drawableRuleset.TimelineTimeRange = EditorClock.TrackLength / screenWithTimeline.TimelineArea.Timeline.CurrentZoom / 2; + drawableRuleset.TimelineTimeRange = EditorClock.TrackLength / screenWithTimeline.TimelineArea.Timeline.CurrentZoom.Value / 2; } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index 1638700735..fda907bf24 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -72,36 +72,31 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline samplesVisible = config.GetBindable(OsuSetting.EditorTimelineShowSamples); } + private BindableNumber? timelineZoom; + protected override void LoadComplete() { base.LoadComplete(); samplesVisible.BindValueChanged(visible => this.FadeTo(visible.NewValue ? 1 : 0, 200, Easing.OutQuint)); this.FadeTo(samplesVisible.Value ? 1 : 0); - } - private float lastZoom; - private const float zoom_threshold = 40f; - - protected override void Update() - { - base.Update(); - - // Retract visual state if the timeline is zoomed out too far. - if (timeline is null || timeline.Zoom == lastZoom) return; - - lastZoom = timeline.Zoom; - - if (timeline.Zoom < zoom_threshold) + timelineZoom = timeline?.CurrentZoom.GetBoundCopy(); + timelineZoom?.BindValueChanged(zoom => { - Label.FadeOut(200, Easing.OutQuint); - LabelContainer.ResizeWidthTo(16, 200, Easing.OutQuint); - } - else - { - Label.FadeIn(200, Easing.OutQuint); - LabelContainer.ResizeWidthTo(Label.Width, 200, Easing.OutQuint); - } + const float zoom_threshold = 40f; + + if (zoom.NewValue < zoom_threshold) + { + Label.FadeOut(200, Easing.OutQuint); + LabelContainer.ResizeWidthTo(16, 200, Easing.OutQuint); + } + else + { + Label.FadeIn(200, Easing.OutQuint); + LabelContainer.ResizeWidthTo(Label.Width, 200, Easing.OutQuint); + } + }, true); } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs index 848c8f9a0f..31a0936eb4 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs @@ -3,6 +3,7 @@ using System; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Transforms; @@ -32,10 +33,10 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline protected override Container Content => zoomedContent; /// - /// The current zoom level of . + /// The current (final) zoom level of . /// It may differ from during transitions. /// - public float CurrentZoom { get; private set; } = 1; + public BindableFloat CurrentZoom { get; private set; } = new BindableFloat(1); private bool isZoomSetUp; @@ -98,7 +99,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline minZoom = minimum; maxZoom = maximum; - CurrentZoom = zoomTarget = initial; + CurrentZoom.Value = zoomTarget = initial; zoomedContentWidthCache.Invalidate(); isZoomSetUp = true; @@ -124,7 +125,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline if (IsLoaded) setZoomTarget(newZoom, ToSpaceOfOtherDrawable(new Vector2(DrawWidth / 2, 0), zoomedContent).X); else - CurrentZoom = zoomTarget = newZoom; + CurrentZoom.Value = zoomTarget = newZoom; } protected override void UpdateAfterChildren() @@ -154,7 +155,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private void updateZoomedContentWidth() { - zoomedContent.Width = DrawWidth * CurrentZoom; + zoomedContent.Width = DrawWidth * CurrentZoom.Value; zoomedContentWidthCache.Validate(); } @@ -238,7 +239,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline float expectedWidth = d.DrawWidth * newZoom; float targetOffset = expectedWidth * (focusPoint / contentSize) - focusOffset; - d.CurrentZoom = newZoom; + d.CurrentZoom.Value = newZoom; d.updateZoomedContentWidth(); // Temporarily here to make sure ScrollTo gets the correct DrawSize for scrollable area. @@ -247,7 +248,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline d.ScrollTo(targetOffset, false); } - protected override void ReadIntoStartValue(ZoomableScrollContainer d) => StartValue = d.CurrentZoom; + protected override void ReadIntoStartValue(ZoomableScrollContainer d) => StartValue = d.CurrentZoom.Value; } } } From 9dcce67b81032cedd9e7d0695d22a70edb70cff0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 27 Sep 2024 17:24:24 +0900 Subject: [PATCH 04/23] Make points slightly smaller when contracted and fix width getting obliterated --- .../Components/Timeline/SamplePointPiece.cs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index fda907bf24..1076255b6a 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -74,6 +74,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private BindableNumber? timelineZoom; + private bool contracted; + protected override void LoadComplete() { base.LoadComplete(); @@ -88,13 +90,17 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline if (zoom.NewValue < zoom_threshold) { + contracted = true; Label.FadeOut(200, Easing.OutQuint); - LabelContainer.ResizeWidthTo(16, 200, Easing.OutQuint); + LabelContainer.ResizeTo(new Vector2(12), 200, Easing.OutQuint); + LabelContainer.CornerRadius = 6; } else { + contracted = false; Label.FadeIn(200, Easing.OutQuint); - LabelContainer.ResizeWidthTo(Label.Width, 200, Easing.OutQuint); + LabelContainer.ResizeTo(new Vector2(Label.Width, 16), 200, Easing.OutQuint); + LabelContainer.CornerRadius = 8; } }, true); } @@ -124,7 +130,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private void updateText() { Label.Text = $"{abbreviateBank(GetBankValue(GetSamples()))} {GetVolumeValue(GetSamples())}"; - LabelContainer.ResizeWidthTo(Label.Width, 200, Easing.OutQuint); + + if (!contracted) + LabelContainer.ResizeWidthTo(Label.Width, 200, Easing.OutQuint); } private static string? abbreviateBank(string? bank) From 48da758c3913f69bcc3cd8cb3096f5fe3a6d5406 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 5 Oct 2024 20:52:45 +0200 Subject: [PATCH 05/23] contract sample point pieces based on smallest gap on the timeline It finds the smallest distance between two sample point pieces on the alive timeline blueprints --- .../Components/Timeline/SamplePointPiece.cs | 20 ++++----- .../Timeline/TimelineBlueprintContainer.cs | 41 +++++++++++++++++++ 2 files changed, 49 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index 1076255b6a..ded7c66058 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -42,7 +42,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private Editor? editor { get; set; } [Resolved] - private Timeline? timeline { get; set; } + private TimelineBlueprintContainer? timelineBlueprintContainer { get; set; } private Bindable samplesVisible = null!; @@ -72,9 +72,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline samplesVisible = config.GetBindable(OsuSetting.EditorTimelineShowSamples); } - private BindableNumber? timelineZoom; - - private bool contracted; + private readonly Bindable contracted = new Bindable(); protected override void LoadComplete() { @@ -83,21 +81,19 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline samplesVisible.BindValueChanged(visible => this.FadeTo(visible.NewValue ? 1 : 0, 200, Easing.OutQuint)); this.FadeTo(samplesVisible.Value ? 1 : 0); - timelineZoom = timeline?.CurrentZoom.GetBoundCopy(); - timelineZoom?.BindValueChanged(zoom => - { - const float zoom_threshold = 40f; + if (timelineBlueprintContainer != null) + contracted.BindTo(timelineBlueprintContainer.SamplePointContracted); - if (zoom.NewValue < zoom_threshold) + contracted.BindValueChanged(v => + { + if (v.NewValue) { - contracted = true; Label.FadeOut(200, Easing.OutQuint); LabelContainer.ResizeTo(new Vector2(12), 200, Easing.OutQuint); LabelContainer.CornerRadius = 6; } else { - contracted = false; Label.FadeIn(200, Easing.OutQuint); LabelContainer.ResizeTo(new Vector2(Label.Width, 16), 200, Easing.OutQuint); LabelContainer.CornerRadius = 8; @@ -131,7 +127,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { Label.Text = $"{abbreviateBank(GetBankValue(GetSamples()))} {GetVolumeValue(GetSamples())}"; - if (!contracted) + if (!contracted.Value) LabelContainer.ResizeWidthTo(Label.Width, 200, Easing.OutQuint); } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs index 740f0b6aac..b50739c80f 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs @@ -15,16 +15,19 @@ using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; +using osu.Framework.Layout; using osu.Framework.Utils; using osu.Game.Graphics; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts; using osuTK; using osuTK.Graphics; namespace osu.Game.Screens.Edit.Compose.Components.Timeline { + [Cached] internal partial class TimelineBlueprintContainer : EditorBlueprintContainer { [Resolved(CanBeNull = true)] @@ -35,6 +38,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private bool hitObjectDragged; + private readonly LayoutValue samplePointContractedStateCache = new LayoutValue(Invalidation.DrawSize); + /// /// Positional input must be received outside the container's bounds, /// in order to handle timeline blueprints which are stacked offscreen. @@ -49,6 +54,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline Origin = Anchor.Centre; Height = 0.6f; + + AddLayout(samplePointContractedStateCache); } [BackgroundDependencyLoader] @@ -116,11 +123,43 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline Composer.Playfield.FutureLifetimeExtension = timeline.VisibleRange / 2; } + updateSamplePointContractedState(); + base.Update(); updateStacking(); } + public Bindable SamplePointContracted = new Bindable(); + + private void updateSamplePointContractedState() + { + if (samplePointContractedStateCache.IsValid) + return; + + const double minimum_gap = 28; + + // Find the smallest time gap between any two sample point pieces + double smallestTimeGap = double.PositiveInfinity; + double lastTime = double.PositiveInfinity; + + // The blueprints are ordered in reverse chronological order + foreach (var selectionBlueprint in SelectionBlueprints) + { + var hitObject = selectionBlueprint.Item; + + if (hitObject is IHasRepeats hasRepeats) + smallestTimeGap = Math.Min(smallestTimeGap, hasRepeats.Duration / hasRepeats.SpanCount() / 2); + + smallestTimeGap = Math.Min(smallestTimeGap, lastTime - hitObject.GetEndTime()); + lastTime = hitObject.StartTime; + } + + double smallestAbsoluteGap = ((TimelineSelectionBlueprintContainer)SelectionBlueprints).ContentRelativeToAbsoluteFactor.X * smallestTimeGap; + SamplePointContracted.Value = smallestAbsoluteGap < minimum_gap; + samplePointContractedStateCache.Validate(); + } + private readonly Stack currentConcurrentObjects = new Stack(); private void updateStacking() @@ -288,6 +327,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { protected override Container> Content { get; } + public Vector2 ContentRelativeToAbsoluteFactor => Content.RelativeToAbsoluteFactor; + public TimelineSelectionBlueprintContainer() { AddInternal(new TimelinePart>(Content = new HitObjectOrderedSelectionContainer { RelativeSizeAxes = Axes.Both }) { RelativeSizeAxes = Axes.Both }); From d195a694479d8086a81bed39a33776138b104084 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 17 Oct 2024 19:26:19 +0900 Subject: [PATCH 06/23] Compute maximum performance along with difficulty --- osu.Game/Beatmaps/BeatmapDifficultyCache.cs | 109 +++++++++++++++- osu.Game/Beatmaps/StarDifficulty.cs | 25 ++-- .../PerformanceBreakdownCalculator.cs | 121 ------------------ .../Statistics/PerformanceStatistic.cs | 4 +- .../Statistics/PerformanceBreakdownChart.cs | 34 ++++- 5 files changed, 151 insertions(+), 142 deletions(-) delete mode 100644 osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs diff --git a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs index 871faf5906..c4506c9f7c 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs @@ -4,12 +4,15 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using osu.Framework.Allocation; +using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Extensions; +using osu.Framework.Graphics.Textures; using osu.Framework.Lists; using osu.Framework.Logging; using osu.Framework.Threading; @@ -18,7 +21,12 @@ using osu.Game.Database; using osu.Game.Rulesets; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; +using osu.Game.Scoring; +using osu.Game.Skinning; +using osu.Game.Storyboards; namespace osu.Game.Beatmaps { @@ -237,10 +245,13 @@ namespace osu.Game.Beatmaps var ruleset = rulesetInfo.CreateInstance(); Debug.Assert(ruleset != null); - var calculator = ruleset.CreateDifficultyCalculator(beatmapManager.GetWorkingBeatmap(key.BeatmapInfo)); - var attributes = calculator.Calculate(key.OrderedMods, cancellationToken); + PlayableCachedWorkingBeatmap working = new PlayableCachedWorkingBeatmap(beatmapManager.GetWorkingBeatmap(key.BeatmapInfo)); - return new StarDifficulty(attributes); + var difficulty = ruleset.CreateDifficultyCalculator(working).Calculate(key.OrderedMods, cancellationToken); + cancellationToken.ThrowIfCancellationRequested(); + var performance = computeMaxPerformance(working, key.BeatmapInfo, ruleset, key.OrderedMods, difficulty); + + return new StarDifficulty(difficulty, performance); } catch (OperationCanceledException) { @@ -262,6 +273,60 @@ namespace osu.Game.Beatmaps } } + private static PerformanceAttributes computeMaxPerformance(IWorkingBeatmap working, BeatmapInfo beatmap, Ruleset ruleset, Mod[] mods, DifficultyAttributes attributes) + { + var performanceCalculator = ruleset.CreatePerformanceCalculator(); + if (performanceCalculator == null) + return new PerformanceAttributes(); + + IBeatmap playableBeatmap = working.GetPlayableBeatmap(ruleset.RulesetInfo, mods); + + // create statistics assuming all hit objects have perfect hit result + var statistics = playableBeatmap.HitObjects + .SelectMany(getPerfectHitResults) + .GroupBy(hr => hr, (hr, list) => (hitResult: hr, count: list.Count())) + .ToDictionary(pair => pair.hitResult, pair => pair.count); + + // compute maximum total score + ScoreProcessor scoreProcessor = ruleset.CreateScoreProcessor(); + scoreProcessor.Mods.Value = mods; + scoreProcessor.ApplyBeatmap(playableBeatmap); + long maxScore = scoreProcessor.MaximumTotalScore; + + // todo: Get max combo from difficulty calculator instead when diffcalc properly supports lazer-first scores + int maxCombo = calculateMaxCombo(playableBeatmap); + + // compute maximum rank - default to SS, then adjust the rank with mods + ScoreRank maxRank = ScoreRank.X; + foreach (IApplicableToScoreProcessor mod in mods.OfType()) + maxRank = mod.AdjustRank(maxRank, 1); + + ScoreInfo perfectScore = new ScoreInfo(beatmap, ruleset.RulesetInfo) + { + Accuracy = 1, + Passed = true, + MaxCombo = maxCombo, + Combo = maxCombo, + Mods = mods, + TotalScore = maxScore, + Statistics = statistics, + MaximumStatistics = statistics + }; + + return performanceCalculator.Calculate(perfectScore, attributes); + + static int calculateMaxCombo(IBeatmap beatmap) + => beatmap.HitObjects.SelectMany(getPerfectHitResults).Count(r => r.AffectsCombo()); + + static IEnumerable getPerfectHitResults(HitObject hitObject) + { + foreach (HitObject nested in hitObject.NestedHitObjects) + yield return nested.Judgement.MaxResult; + + yield return hitObject.Judgement.MaxResult; + } + } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); @@ -276,7 +341,6 @@ namespace osu.Game.Beatmaps { public readonly BeatmapInfo BeatmapInfo; public readonly RulesetInfo Ruleset; - public readonly Mod[] OrderedMods; public DifficultyCacheLookup(BeatmapInfo beatmapInfo, RulesetInfo? ruleset, IEnumerable? mods) @@ -317,5 +381,42 @@ namespace osu.Game.Beatmaps CancellationToken = cancellationToken; } } + + /// + /// A working beatmap that caches its playable representation. + /// This is intended as single-use for when it is guaranteed that the playable beatmap can be reused. + /// + private class PlayableCachedWorkingBeatmap : IWorkingBeatmap + { + private readonly IWorkingBeatmap working; + private IBeatmap? playable; + + public PlayableCachedWorkingBeatmap(IWorkingBeatmap working) + { + this.working = working; + } + + public IBeatmap GetPlayableBeatmap(IRulesetInfo ruleset, IReadOnlyList mods) + => playable ??= working.GetPlayableBeatmap(ruleset, mods); + + public IBeatmap GetPlayableBeatmap(IRulesetInfo ruleset, IReadOnlyList mods, CancellationToken cancellationToken) + => playable ??= working.GetPlayableBeatmap(ruleset, mods, cancellationToken); + + IBeatmapInfo IWorkingBeatmap.BeatmapInfo => working.BeatmapInfo; + bool IWorkingBeatmap.BeatmapLoaded => working.BeatmapLoaded; + bool IWorkingBeatmap.TrackLoaded => working.TrackLoaded; + IBeatmap IWorkingBeatmap.Beatmap => working.Beatmap; + Texture IWorkingBeatmap.GetBackground() => working.GetBackground(); + Texture IWorkingBeatmap.GetPanelBackground() => working.GetPanelBackground(); + Waveform IWorkingBeatmap.Waveform => working.Waveform; + Storyboard IWorkingBeatmap.Storyboard => working.Storyboard; + ISkin IWorkingBeatmap.Skin => working.Skin; + Track IWorkingBeatmap.Track => working.Track; + Track IWorkingBeatmap.LoadTrack() => working.LoadTrack(); + Stream IWorkingBeatmap.GetStream(string storagePath) => working.GetStream(storagePath); + void IWorkingBeatmap.BeginAsyncLoad() => working.BeginAsyncLoad(); + void IWorkingBeatmap.CancelAsyncLoad() => working.CancelAsyncLoad(); + void IWorkingBeatmap.PrepareTrackForPreview(bool looping, double offsetFromPreviewPoint) => working.PrepareTrackForPreview(looping, offsetFromPreviewPoint); + } } } diff --git a/osu.Game/Beatmaps/StarDifficulty.cs b/osu.Game/Beatmaps/StarDifficulty.cs index 6aac275a6a..51358fcd7f 100644 --- a/osu.Game/Beatmaps/StarDifficulty.cs +++ b/osu.Game/Beatmaps/StarDifficulty.cs @@ -26,29 +26,36 @@ namespace osu.Game.Beatmaps /// Might not be available if the star difficulty is associated with a beatmap that's not locally available. /// [CanBeNull] - public readonly DifficultyAttributes Attributes; + public readonly DifficultyAttributes DifficultyAttributes; /// - /// Creates a structure based on computed - /// by a . + /// The performance attributes computed for a perfect score on the given beatmap. + /// Might not be available if the star difficulty is associated with a beatmap that's not locally available. /// - public StarDifficulty([NotNull] DifficultyAttributes attributes) + [CanBeNull] + public readonly PerformanceAttributes PerformanceAttributes; + + /// + /// Creates a structure. + /// + public StarDifficulty([NotNull] DifficultyAttributes difficulty, [NotNull] PerformanceAttributes performance) { - Stars = double.IsFinite(attributes.StarRating) ? attributes.StarRating : 0; - MaxCombo = attributes.MaxCombo; - Attributes = attributes; + Stars = double.IsFinite(difficulty.StarRating) ? difficulty.StarRating : 0; + MaxCombo = difficulty.MaxCombo; + DifficultyAttributes = difficulty; + PerformanceAttributes = performance; // Todo: Add more members (BeatmapInfo.DifficultyRating? Attributes? Etc...) } /// /// Creates a structure with a pre-populated star difficulty and max combo - /// in scenarios where computing is not feasible (i.e. when working with online sources). + /// in scenarios where computing is not feasible (i.e. when working with online sources). /// public StarDifficulty(double starDifficulty, int maxCombo) { Stars = double.IsFinite(starDifficulty) ? starDifficulty : 0; MaxCombo = maxCombo; - Attributes = null; + DifficultyAttributes = null; } public DifficultyRating DifficultyRating => GetDifficultyRating(Stars); diff --git a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs deleted file mode 100644 index 946d83b14b..0000000000 --- a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs +++ /dev/null @@ -1,121 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -#nullable disable - -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using JetBrains.Annotations; -using osu.Framework.Extensions.ObjectExtensions; -using osu.Game.Beatmaps; -using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Scoring; -using osu.Game.Scoring; - -namespace osu.Game.Rulesets.Difficulty -{ - public class PerformanceBreakdownCalculator - { - private readonly IBeatmap playableBeatmap; - private readonly BeatmapDifficultyCache difficultyCache; - - public PerformanceBreakdownCalculator(IBeatmap playableBeatmap, BeatmapDifficultyCache difficultyCache) - { - this.playableBeatmap = playableBeatmap; - this.difficultyCache = difficultyCache; - } - - [ItemCanBeNull] - public async Task CalculateAsync(ScoreInfo score, CancellationToken cancellationToken = default) - { - var attributes = await difficultyCache.GetDifficultyAsync(score.BeatmapInfo!, score.Ruleset, score.Mods, cancellationToken).ConfigureAwait(false); - - var performanceCalculator = score.Ruleset.CreateInstance().CreatePerformanceCalculator(); - - // Performance calculation requires the beatmap and ruleset to be locally available. If not, return a default value. - if (attributes?.Attributes == null || performanceCalculator == null) - return null; - - cancellationToken.ThrowIfCancellationRequested(); - - PerformanceAttributes[] performanceArray = await Task.WhenAll( - // compute actual performance - performanceCalculator.CalculateAsync(score, attributes.Value.Attributes, cancellationToken), - // compute performance for perfect play - getPerfectPerformance(score, cancellationToken) - ).ConfigureAwait(false); - - return new PerformanceBreakdown(performanceArray[0] ?? new PerformanceAttributes(), performanceArray[1] ?? new PerformanceAttributes()); - } - - [ItemCanBeNull] - private Task getPerfectPerformance(ScoreInfo score, CancellationToken cancellationToken = default) - { - return Task.Run(async () => - { - Ruleset ruleset = score.Ruleset.CreateInstance(); - ScoreInfo perfectPlay = score.DeepClone(); - perfectPlay.Accuracy = 1; - perfectPlay.Passed = true; - - // calculate max combo - // todo: Get max combo from difficulty calculator instead when diffcalc properly supports lazer-first scores - perfectPlay.MaxCombo = calculateMaxCombo(playableBeatmap); - - // create statistics assuming all hit objects have perfect hit result - var statistics = playableBeatmap.HitObjects - .SelectMany(getPerfectHitResults) - .GroupBy(hr => hr, (hr, list) => (hitResult: hr, count: list.Count())) - .ToDictionary(pair => pair.hitResult, pair => pair.count); - perfectPlay.Statistics = statistics; - perfectPlay.MaximumStatistics = statistics; - - // calculate total score - ScoreProcessor scoreProcessor = ruleset.CreateScoreProcessor(); - scoreProcessor.Mods.Value = perfectPlay.Mods; - scoreProcessor.ApplyBeatmap(playableBeatmap); - perfectPlay.TotalScore = scoreProcessor.MaximumTotalScore; - - // compute rank achieved - // default to SS, then adjust the rank with mods - perfectPlay.Rank = ScoreRank.X; - - foreach (IApplicableToScoreProcessor mod in perfectPlay.Mods.OfType()) - { - perfectPlay.Rank = mod.AdjustRank(perfectPlay.Rank, 1); - } - - // calculate performance for this perfect score - var difficulty = await difficultyCache.GetDifficultyAsync( - playableBeatmap.BeatmapInfo, - score.Ruleset, - score.Mods, - cancellationToken - ).ConfigureAwait(false); - - var performanceCalculator = ruleset.CreatePerformanceCalculator(); - - if (performanceCalculator == null || difficulty == null) - return null; - - return await performanceCalculator.CalculateAsync(perfectPlay, difficulty.Value.Attributes.AsNonNull(), cancellationToken).ConfigureAwait(false); - }, cancellationToken); - } - - private int calculateMaxCombo(IBeatmap beatmap) - { - return beatmap.HitObjects.SelectMany(getPerfectHitResults).Count(r => r.AffectsCombo()); - } - - private IEnumerable getPerfectHitResults(HitObject hitObject) - { - foreach (HitObject nested in hitObject.NestedHitObjects) - yield return nested.Judgement.MaxResult; - - yield return hitObject.Judgement.MaxResult; - } - } -} diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs index 7ea3cbe917..7d155e32b0 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs @@ -53,10 +53,10 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics var performanceCalculator = score.Ruleset.CreateInstance().CreatePerformanceCalculator(); // Performance calculation requires the beatmap and ruleset to be locally available. If not, return a default value. - if (attributes?.Attributes == null || performanceCalculator == null) + if (attributes?.DifficultyAttributes == null || performanceCalculator == null) return; - var result = await performanceCalculator.CalculateAsync(score, attributes.Value.Attributes, cancellationToken ?? default).ConfigureAwait(false); + var result = await performanceCalculator.CalculateAsync(score, attributes.Value.DifficultyAttributes, cancellationToken ?? default).ConfigureAwait(false); Schedule(() => setPerformanceValue(score, result.Total)); }, cancellationToken ?? default); diff --git a/osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs b/osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs index b5eed2d12a..f9c8c93dec 100644 --- a/osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs +++ b/osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading; +using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Extensions; using osu.Framework.Extensions.Color4Extensions; @@ -26,7 +27,6 @@ namespace osu.Game.Screens.Ranking.Statistics public partial class PerformanceBreakdownChart : Container { private readonly ScoreInfo score; - private readonly IBeatmap playableBeatmap; private Drawable spinner = null!; private Drawable content = null!; @@ -42,7 +42,6 @@ namespace osu.Game.Screens.Ranking.Statistics public PerformanceBreakdownChart(ScoreInfo score, IBeatmap playableBeatmap) { this.score = score; - this.playableBeatmap = playableBeatmap; } [BackgroundDependencyLoader] @@ -142,12 +141,33 @@ namespace osu.Game.Screens.Ranking.Statistics spinner.Show(); - new PerformanceBreakdownCalculator(playableBeatmap, difficultyCache) - .CalculateAsync(score, cancellationTokenSource.Token) - .ContinueWith(t => Schedule(() => setPerformanceValue(t.GetResultSafely()!))); + computePerformance(cancellationTokenSource.Token) + .ContinueWith(t => Schedule(() => + { + if (t.GetResultSafely() is PerformanceBreakdown breakdown) + setPerformance(breakdown); + }), TaskContinuationOptions.OnlyOnRanToCompletion); } - private void setPerformanceValue(PerformanceBreakdown breakdown) + private async Task computePerformance(CancellationToken token) + { + var performanceCalculator = score.Ruleset.CreateInstance().CreatePerformanceCalculator(); + if (performanceCalculator == null) + return null; + + var starsTask = difficultyCache.GetDifficultyAsync(score.BeatmapInfo!, score.Ruleset, score.Mods, token).ConfigureAwait(false); + if (await starsTask is not StarDifficulty stars) + return null; + + if (stars.DifficultyAttributes == null || stars.PerformanceAttributes == null) + return null; + + return new PerformanceBreakdown( + await performanceCalculator.CalculateAsync(score, stars.DifficultyAttributes, token).ConfigureAwait(false), + stars.PerformanceAttributes); + } + + private void setPerformance(PerformanceBreakdown breakdown) { spinner.Hide(); content.FadeIn(200); @@ -236,6 +256,8 @@ namespace osu.Game.Screens.Ranking.Statistics protected override void Dispose(bool isDisposing) { cancellationTokenSource.Cancel(); + cancellationTokenSource.Dispose(); + base.Dispose(isDisposing); } } From bfcf6693cac611de1deb8909a76a15ef7f8517f2 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 17 Oct 2024 19:39:03 +0900 Subject: [PATCH 07/23] Simplify implementation --- osu.Game/Beatmaps/BeatmapDifficultyCache.cs | 85 +++++++-------------- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 6 ++ 2 files changed, 33 insertions(+), 58 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs index c4506c9f7c..fc4175415c 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs @@ -21,7 +21,6 @@ using osu.Game.Database; using osu.Game.Rulesets; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Scoring; @@ -245,11 +244,35 @@ namespace osu.Game.Beatmaps var ruleset = rulesetInfo.CreateInstance(); Debug.Assert(ruleset != null); - PlayableCachedWorkingBeatmap working = new PlayableCachedWorkingBeatmap(beatmapManager.GetWorkingBeatmap(key.BeatmapInfo)); + PlayableCachedWorkingBeatmap workingBeatmap = new PlayableCachedWorkingBeatmap(beatmapManager.GetWorkingBeatmap(key.BeatmapInfo)); + IBeatmap playableBeatmap = workingBeatmap.GetPlayableBeatmap(ruleset.RulesetInfo, key.OrderedMods, cancellationToken); - var difficulty = ruleset.CreateDifficultyCalculator(working).Calculate(key.OrderedMods, cancellationToken); + var difficulty = ruleset.CreateDifficultyCalculator(workingBeatmap).Calculate(key.OrderedMods, cancellationToken); + cancellationToken.ThrowIfCancellationRequested(); + + var performanceCalculator = ruleset.CreatePerformanceCalculator(); + if (performanceCalculator == null) + return new StarDifficulty(difficulty, new PerformanceAttributes()); + + ScoreProcessor scoreProcessor = ruleset.CreateScoreProcessor(); + scoreProcessor.Mods.Value = key.OrderedMods; + scoreProcessor.ApplyBeatmap(playableBeatmap); + cancellationToken.ThrowIfCancellationRequested(); + + ScoreInfo perfectScore = new ScoreInfo(key.BeatmapInfo, ruleset.RulesetInfo) + { + Passed = true, + Accuracy = 1, + Mods = key.OrderedMods, + MaxCombo = scoreProcessor.MaximumCombo, + Combo = scoreProcessor.MaximumCombo, + TotalScore = scoreProcessor.MaximumTotalScore, + Statistics = scoreProcessor.MaximumStatistics, + MaximumStatistics = scoreProcessor.MaximumStatistics + }; + + var performance = performanceCalculator.Calculate(perfectScore, difficulty); cancellationToken.ThrowIfCancellationRequested(); - var performance = computeMaxPerformance(working, key.BeatmapInfo, ruleset, key.OrderedMods, difficulty); return new StarDifficulty(difficulty, performance); } @@ -273,60 +296,6 @@ namespace osu.Game.Beatmaps } } - private static PerformanceAttributes computeMaxPerformance(IWorkingBeatmap working, BeatmapInfo beatmap, Ruleset ruleset, Mod[] mods, DifficultyAttributes attributes) - { - var performanceCalculator = ruleset.CreatePerformanceCalculator(); - if (performanceCalculator == null) - return new PerformanceAttributes(); - - IBeatmap playableBeatmap = working.GetPlayableBeatmap(ruleset.RulesetInfo, mods); - - // create statistics assuming all hit objects have perfect hit result - var statistics = playableBeatmap.HitObjects - .SelectMany(getPerfectHitResults) - .GroupBy(hr => hr, (hr, list) => (hitResult: hr, count: list.Count())) - .ToDictionary(pair => pair.hitResult, pair => pair.count); - - // compute maximum total score - ScoreProcessor scoreProcessor = ruleset.CreateScoreProcessor(); - scoreProcessor.Mods.Value = mods; - scoreProcessor.ApplyBeatmap(playableBeatmap); - long maxScore = scoreProcessor.MaximumTotalScore; - - // todo: Get max combo from difficulty calculator instead when diffcalc properly supports lazer-first scores - int maxCombo = calculateMaxCombo(playableBeatmap); - - // compute maximum rank - default to SS, then adjust the rank with mods - ScoreRank maxRank = ScoreRank.X; - foreach (IApplicableToScoreProcessor mod in mods.OfType()) - maxRank = mod.AdjustRank(maxRank, 1); - - ScoreInfo perfectScore = new ScoreInfo(beatmap, ruleset.RulesetInfo) - { - Accuracy = 1, - Passed = true, - MaxCombo = maxCombo, - Combo = maxCombo, - Mods = mods, - TotalScore = maxScore, - Statistics = statistics, - MaximumStatistics = statistics - }; - - return performanceCalculator.Calculate(perfectScore, attributes); - - static int calculateMaxCombo(IBeatmap beatmap) - => beatmap.HitObjects.SelectMany(getPerfectHitResults).Count(r => r.AffectsCombo()); - - static IEnumerable getPerfectHitResults(HitObject hitObject) - { - foreach (HitObject nested in hitObject.NestedHitObjects) - yield return nested.Judgement.MaxResult; - - yield return hitObject.Judgement.MaxResult; - } - } - protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 9752918dfb..7b5af9beda 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -119,6 +119,11 @@ namespace osu.Game.Rulesets.Scoring /// public long MaximumTotalScore { get; private set; } + /// + /// The maximum achievable combo. + /// + public int MaximumCombo { get; private set; } + /// /// The maximum sum of accuracy-affecting judgements at the current point in time. /// @@ -423,6 +428,7 @@ namespace osu.Game.Rulesets.Scoring MaximumResultCounts.AddRange(ScoreResultCounts); MaximumTotalScore = TotalScore.Value; + MaximumCombo = HighestCombo.Value; } ScoreResultCounts.Clear(); From 6749768b9e1a1b4d095581ade5359e7927f1e34a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 17 Oct 2024 19:43:34 +0900 Subject: [PATCH 08/23] Add max performance beatmap attribute text --- .../SkinComponents/BeatmapAttributeTextStrings.cs | 13 +++++++++---- .../Skinning/Components/BeatmapAttributeText.cs | 10 ++++++++++ 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/osu.Game/Localisation/SkinComponents/BeatmapAttributeTextStrings.cs b/osu.Game/Localisation/SkinComponents/BeatmapAttributeTextStrings.cs index b2e2285faf..390a6f9ca4 100644 --- a/osu.Game/Localisation/SkinComponents/BeatmapAttributeTextStrings.cs +++ b/osu.Game/Localisation/SkinComponents/BeatmapAttributeTextStrings.cs @@ -12,23 +12,28 @@ namespace osu.Game.Localisation.SkinComponents /// /// "Attribute" /// - public static LocalisableString Attribute => new TranslatableString(getKey(@"attribute"), "Attribute"); + public static LocalisableString Attribute => new TranslatableString(getKey(@"attribute"), @"Attribute"); /// /// "The attribute to be displayed." /// - public static LocalisableString AttributeDescription => new TranslatableString(getKey(@"attribute_description"), "The attribute to be displayed."); + public static LocalisableString AttributeDescription => new TranslatableString(getKey(@"attribute_description"), @"The attribute to be displayed."); /// /// "Template" /// - public static LocalisableString Template => new TranslatableString(getKey(@"template"), "Template"); + public static LocalisableString Template => new TranslatableString(getKey(@"template"), @"Template"); /// /// "Supports {{Label}} and {{Value}}, but also including arbitrary attributes like {{StarRating}} (see attribute list for supported values)." /// public static LocalisableString TemplateDescription => new TranslatableString(getKey(@"template_description"), @"Supports {{Label}} and {{Value}}, but also including arbitrary attributes like {{StarRating}} (see attribute list for supported values)."); - private static string getKey(string key) => $"{prefix}:{key}"; + /// + /// "Max PP" + /// + public static LocalisableString MaxPP => new TranslatableString(getKey(@"max_pp"), @"Max PP"); + + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Skinning/Components/BeatmapAttributeText.cs b/osu.Game/Skinning/Components/BeatmapAttributeText.cs index ccee90410e..28352b533f 100644 --- a/osu.Game/Skinning/Components/BeatmapAttributeText.cs +++ b/osu.Game/Skinning/Components/BeatmapAttributeText.cs @@ -21,6 +21,7 @@ using osu.Game.Localisation; using osu.Game.Localisation.SkinComponents; using osu.Game.Resources.Localisation.Web; using osu.Game.Rulesets; +using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Mods; using osu.Game.Utils; @@ -167,6 +168,9 @@ namespace osu.Game.Skinning.Components case BeatmapAttribute.BPM: return BeatmapsetsStrings.ShowStatsBpm; + case BeatmapAttribute.MaxPP: + return BeatmapAttributeTextStrings.MaxPP; + default: throw new ArgumentOutOfRangeException(); } @@ -217,6 +221,11 @@ namespace osu.Game.Skinning.Components ? starDifficulty.Stars.ToLocalisableString(@"F2") : @"..."; + case BeatmapAttribute.MaxPP: + return difficultyBindable?.Value?.PerformanceAttributes is PerformanceAttributes attributes + ? attributes.Total.ToLocalisableString(@"F2") + : @"..."; + default: throw new ArgumentOutOfRangeException(); } @@ -269,5 +278,6 @@ namespace osu.Game.Skinning.Components RankedStatus, BPM, Source, + MaxPP } } From 0473f0b60a58a0b0b733f9125ed121022c1fc6d1 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 17 Oct 2024 20:39:24 +0900 Subject: [PATCH 09/23] Use stored difficulty to reduce flickering --- osu.Game/Skinning/Components/BeatmapAttributeText.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game/Skinning/Components/BeatmapAttributeText.cs b/osu.Game/Skinning/Components/BeatmapAttributeText.cs index bfdb7655fc..1b4f853d72 100644 --- a/osu.Game/Skinning/Components/BeatmapAttributeText.cs +++ b/osu.Game/Skinning/Components/BeatmapAttributeText.cs @@ -21,7 +21,6 @@ using osu.Game.Localisation; using osu.Game.Localisation.SkinComponents; using osu.Game.Resources.Localisation.Web; using osu.Game.Rulesets; -using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Mods; using osu.Game.Utils; @@ -225,9 +224,7 @@ namespace osu.Game.Skinning.Components return (starDifficulty?.Stars ?? 0).ToLocalisableString(@"F2"); case BeatmapAttribute.MaxPP: - return difficultyBindable?.Value?.PerformanceAttributes is PerformanceAttributes attributes - ? attributes.Total.ToLocalisableString(@"F2") - : @"..."; + return (starDifficulty?.PerformanceAttributes?.Total ?? 0).ToLocalisableString(@"F2"); default: throw new ArgumentOutOfRangeException(); From 03094533b481cc2365b59d2b9c5e9b51a77f08f4 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 17 Oct 2024 22:03:53 +0900 Subject: [PATCH 10/23] Add test --- .../TestSceneBeatmapAttributeText.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapAttributeText.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapAttributeText.cs index 01659a2654..5c929a4d62 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapAttributeText.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapAttributeText.cs @@ -159,6 +159,23 @@ namespace osu.Game.Tests.Visual.UserInterface AddUntilStep("check star rating is 2", getText, () => Is.EqualTo("Star Rating: 2.00")); } + [Test] + public void TestMaxPp() + { + AddStep("set test ruleset", () => Ruleset.Value = new TestRuleset().RulesetInfo); + AddStep("set max pp attribute", () => text.Attribute.Value = BeatmapAttribute.MaxPP); + AddAssert("check max pp is 0", getText, () => Is.EqualTo("Max PP: 0.00")); + + // Adding mod + TestMod mod = null!; + AddStep("add mod with pp 1", () => SelectedMods.Value = new[] { mod = new TestMod { Performance = { Value = 1 } } }); + AddUntilStep("check max pp is 1", getText, () => Is.EqualTo("Max PP: 1.00")); + + // Changing mod setting + AddStep("change mod pp to 2", () => mod.Performance.Value = 2); + AddUntilStep("check max pp is 2", getText, () => Is.EqualTo("Max PP: 2.00")); + } + private string getText() => text.ChildrenOfType().Single().Text.ToString(); private class TestRuleset : Ruleset From c03017438e3c78fb0e13cdf28fb2fb426b13f363 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 19 Oct 2024 23:57:05 +0200 Subject: [PATCH 11/23] Use FinishTransforms to init visual state --- .../Edit/Compose/Components/Timeline/SamplePointPiece.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index ded7c66058..48178b2e19 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -78,9 +78,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { base.LoadComplete(); - samplesVisible.BindValueChanged(visible => this.FadeTo(visible.NewValue ? 1 : 0, 200, Easing.OutQuint)); - this.FadeTo(samplesVisible.Value ? 1 : 0); - if (timelineBlueprintContainer != null) contracted.BindTo(timelineBlueprintContainer.SamplePointContracted); @@ -99,6 +96,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline LabelContainer.CornerRadius = 8; } }, true); + + samplesVisible.BindValueChanged(visible => this.FadeTo(visible.NewValue ? 1 : 0, 200, Easing.OutQuint), true); + FinishTransforms(); } protected override void Dispose(bool isDisposing) From f137fed04e16b6a1829073b673571e6d6e475376 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 19 Oct 2024 23:57:41 +0200 Subject: [PATCH 12/23] Update sample point contracted state each frame --- .../Timeline/TimelineBlueprintContainer.cs | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs index b50739c80f..fec15517a4 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs @@ -15,7 +15,6 @@ using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; -using osu.Framework.Layout; using osu.Framework.Utils; using osu.Game.Graphics; using osu.Game.Rulesets.Edit; @@ -38,8 +37,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private bool hitObjectDragged; - private readonly LayoutValue samplePointContractedStateCache = new LayoutValue(Invalidation.DrawSize); - /// /// Positional input must be received outside the container's bounds, /// in order to handle timeline blueprints which are stacked offscreen. @@ -54,8 +51,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline Origin = Anchor.Centre; Height = 0.6f; - - AddLayout(samplePointContractedStateCache); } [BackgroundDependencyLoader] @@ -123,10 +118,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline Composer.Playfield.FutureLifetimeExtension = timeline.VisibleRange / 2; } - updateSamplePointContractedState(); - base.Update(); + updateSamplePointContractedState(); updateStacking(); } @@ -134,8 +128,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private void updateSamplePointContractedState() { - if (samplePointContractedStateCache.IsValid) - return; + // because only blueprints of objects which are alive (via pooling) are displayed in the timeline, it's feasible to do this every-update. const double minimum_gap = 28; @@ -157,7 +150,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline double smallestAbsoluteGap = ((TimelineSelectionBlueprintContainer)SelectionBlueprints).ContentRelativeToAbsoluteFactor.X * smallestTimeGap; SamplePointContracted.Value = smallestAbsoluteGap < minimum_gap; - samplePointContractedStateCache.Validate(); } private readonly Stack currentConcurrentObjects = new Stack(); From 22aef90cb7b1edaff97c86ec640ce49b750c6b1f Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sun, 20 Oct 2024 00:07:40 +0200 Subject: [PATCH 13/23] dont contract samples for stacked objects --- .../Components/Timeline/TimelineBlueprintContainer.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs index fec15517a4..c8448b745e 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs @@ -144,7 +144,13 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline if (hitObject is IHasRepeats hasRepeats) smallestTimeGap = Math.Min(smallestTimeGap, hasRepeats.Duration / hasRepeats.SpanCount() / 2); - smallestTimeGap = Math.Min(smallestTimeGap, lastTime - hitObject.GetEndTime()); + double gap = lastTime - hitObject.GetEndTime(); + + // If the gap is less than 1ms, we can assume that the objects are stacked on top of each other + // Contracting doesn't make sense in this case + if (gap > 1 && gap < smallestTimeGap) + smallestTimeGap = gap; + lastTime = hitObject.StartTime; } From eb61da423299e50de211f10cd1e093f1ac444207 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Thu, 24 Oct 2024 21:22:09 +0200 Subject: [PATCH 14/23] only consider hit objects which are visible in timeline for contracted state --- .../Timeline/TimelineBlueprintContainer.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs index c8448b745e..d4b9101747 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs @@ -32,6 +32,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline [Resolved(CanBeNull = true)] private Timeline timeline { get; set; } + [Resolved(CanBeNull = true)] + private EditorClock editorClock { get; set; } + private Bindable placement; private SelectionBlueprint placementBlueprint; @@ -128,10 +131,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private void updateSamplePointContractedState() { - // because only blueprints of objects which are alive (via pooling) are displayed in the timeline, it's feasible to do this every-update. - const double minimum_gap = 28; + if (timeline == null || editorClock == null) + return; + // Find the smallest time gap between any two sample point pieces double smallestTimeGap = double.PositiveInfinity; double lastTime = double.PositiveInfinity; @@ -141,6 +145,14 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { var hitObject = selectionBlueprint.Item; + // Only check the hit objects which are visible in the timeline + // SelectionBlueprints can contain hit objects which are not visible in the timeline due to selection keeping them alive + if (hitObject.StartTime > editorClock.CurrentTime + timeline.VisibleRange / 2) + continue; + + if (hitObject.GetEndTime() < editorClock.CurrentTime - timeline.VisibleRange / 2) + break; + if (hitObject is IHasRepeats hasRepeats) smallestTimeGap = Math.Min(smallestTimeGap, hasRepeats.Duration / hasRepeats.SpanCount() / 2); From ef22b6b1a86afa8998ec6cb7c482ba28bbf097d0 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 1 Nov 2024 22:40:00 +0900 Subject: [PATCH 15/23] Round to integral units The rounding matches the implementation of `PerformancePointsCounter`. --- osu.Game/Skinning/Components/BeatmapAttributeText.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Skinning/Components/BeatmapAttributeText.cs b/osu.Game/Skinning/Components/BeatmapAttributeText.cs index c858e75032..79a1ed4d7c 100644 --- a/osu.Game/Skinning/Components/BeatmapAttributeText.cs +++ b/osu.Game/Skinning/Components/BeatmapAttributeText.cs @@ -229,7 +229,7 @@ namespace osu.Game.Skinning.Components return (starDifficulty?.Stars ?? 0).ToLocalisableString(@"F2"); case BeatmapAttribute.MaxPP: - return (starDifficulty?.PerformanceAttributes?.Total ?? 0).ToLocalisableString(@"F2"); + return Math.Round(starDifficulty?.PerformanceAttributes?.Total ?? 0, MidpointRounding.AwayFromZero).ToLocalisableString(); default: return string.Empty; From 03fc744e927e2cab99f2d43f9e289c75a70dba61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 1 Nov 2024 18:26:49 +0100 Subject: [PATCH 16/23] Fix test --- .../Visual/UserInterface/TestSceneBeatmapAttributeText.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapAttributeText.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapAttributeText.cs index df128ed89e..e3a6fca319 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapAttributeText.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapAttributeText.cs @@ -164,16 +164,16 @@ namespace osu.Game.Tests.Visual.UserInterface { AddStep("set test ruleset", () => Ruleset.Value = new TestRuleset().RulesetInfo); AddStep("set max pp attribute", () => text.Attribute.Value = BeatmapAttribute.MaxPP); - AddAssert("check max pp is 0", getText, () => Is.EqualTo("Max PP: 0.00")); + AddAssert("check max pp is 0", getText, () => Is.EqualTo("Max PP: 0")); // Adding mod TestMod mod = null!; AddStep("add mod with pp 1", () => SelectedMods.Value = new[] { mod = new TestMod { Performance = { Value = 1 } } }); - AddUntilStep("check max pp is 1", getText, () => Is.EqualTo("Max PP: 1.00")); + AddUntilStep("check max pp is 1", getText, () => Is.EqualTo("Max PP: 1")); // Changing mod setting AddStep("change mod pp to 2", () => mod.Performance.Value = 2); - AddUntilStep("check max pp is 2", getText, () => Is.EqualTo("Max PP: 2.00")); + AddUntilStep("check max pp is 2", getText, () => Is.EqualTo("Max PP: 2")); } private string getText() => text.ChildrenOfType().Single().Text.ToString(); From 8f48682d0a8a8385d2ade6db7ce0f03eac1668b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 1 Nov 2024 18:36:01 +0100 Subject: [PATCH 17/23] Remove hide sample toggle In line with feedback from https://github.com/ppy/osu/pull/29896#issuecomment-2378676851. --- osu.Game/Configuration/OsuConfigManager.cs | 2 -- osu.Game/Localisation/EditorStrings.cs | 5 ----- .../Edit/Compose/Components/Timeline/SamplePointPiece.cs | 5 ----- osu.Game/Screens/Edit/Editor.cs | 6 ------ 4 files changed, 18 deletions(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index ae2025fb50..8705253b7d 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -205,7 +205,6 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.EditorTimelineShowTimingChanges, true); SetDefault(OsuSetting.EditorTimelineShowTicks, true); - SetDefault(OsuSetting.EditorTimelineShowSamples, true); SetDefault(OsuSetting.AlwaysShowHoldForMenuButton, false); } @@ -433,6 +432,5 @@ namespace osu.Game.Configuration EditorTimelineShowTimingChanges, EditorTimelineShowTicks, AlwaysShowHoldForMenuButton, - EditorTimelineShowSamples } } diff --git a/osu.Game/Localisation/EditorStrings.cs b/osu.Game/Localisation/EditorStrings.cs index f72a0f4a26..bcffc18d4d 100644 --- a/osu.Game/Localisation/EditorStrings.cs +++ b/osu.Game/Localisation/EditorStrings.cs @@ -139,11 +139,6 @@ namespace osu.Game.Localisation /// public static LocalisableString TimelineShowTicks => new TranslatableString(getKey(@"timeline_show_ticks"), @"Show ticks"); - /// - /// "Show samples" - /// - public static LocalisableString TimelineShowSamples => new TranslatableString(getKey(@"timeline_show_samples"), @"Show samples"); - private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index 48178b2e19..0cfaeb8c38 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -44,8 +44,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline [Resolved] private TimelineBlueprintContainer? timelineBlueprintContainer { get; set; } - private Bindable samplesVisible = null!; - public SamplePointPiece(HitObject hitObject) { HitObject = hitObject; @@ -68,8 +66,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline if (editor != null) editor.ShowSampleEditPopoverRequested += onShowSampleEditPopoverRequested; - - samplesVisible = config.GetBindable(OsuSetting.EditorTimelineShowSamples); } private readonly Bindable contracted = new Bindable(); @@ -97,7 +93,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline } }, true); - samplesVisible.BindValueChanged(visible => this.FadeTo(visible.NewValue ? 1 : 0, 200, Easing.OutQuint), true); FinishTransforms(); } diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index b059557b28..9bb91af806 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -215,7 +215,6 @@ namespace osu.Game.Screens.Edit private Bindable editorLimitedDistanceSnap; private Bindable editorTimelineShowTimingChanges; private Bindable editorTimelineShowTicks; - private Bindable editorTimelineShowSamples; /// /// This controls the opacity of components like the timelines, sidebars, etc. @@ -324,7 +323,6 @@ namespace osu.Game.Screens.Edit editorLimitedDistanceSnap = config.GetBindable(OsuSetting.EditorLimitedDistanceSnap); editorTimelineShowTimingChanges = config.GetBindable(OsuSetting.EditorTimelineShowTimingChanges); editorTimelineShowTicks = config.GetBindable(OsuSetting.EditorTimelineShowTicks); - editorTimelineShowSamples = config.GetBindable(OsuSetting.EditorTimelineShowSamples); AddInternal(new OsuContextMenuContainer { @@ -390,10 +388,6 @@ namespace osu.Game.Screens.Edit { State = { BindTarget = editorTimelineShowTicks } }, - new ToggleMenuItem(EditorStrings.TimelineShowSamples) - { - State = { BindTarget = editorTimelineShowSamples } - } ] }, new BackgroundDimMenuItem(editorBackgroundDim), From e1d93a7d9c819eb2ae792934c299fbf00139aab3 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 11 Nov 2024 15:09:13 +0900 Subject: [PATCH 18/23] Merge implementations of ConvertHitObjectParser Having these be separate implementations sounded awesome at the time, but it only ever led to confusion. There's no practical difference if, for example, catch sees hitobjects with `IHasPosition` instead of `IHasXPosition`. --- .../Beatmaps/Formats/LegacyBeatmapDecoder.cs | 45 ++----- .../Objects/Legacy/Catch/ConvertHit.cs | 20 --- .../Legacy/Catch/ConvertHitObjectParser.cs | 63 --------- .../Objects/Legacy/Catch/ConvertSlider.cs | 20 --- .../Objects/Legacy/Catch/ConvertSpinner.cs | 19 --- .../Objects/Legacy/ConvertHitCircle.cs | 13 ++ .../Objects/Legacy/ConvertHitObject.cs | 14 +- .../Objects/Legacy/ConvertHitObjectParser.cs | 123 ++++++++++++------ .../ConvertSpinner.cs => ConvertHold.cs} | 11 +- .../Rulesets/Objects/Legacy/ConvertSlider.cs | 12 +- .../Legacy/{Taiko => }/ConvertSpinner.cs | 7 +- .../Objects/Legacy/Mania/ConvertHit.cs | 15 --- .../Legacy/Mania/ConvertHitObjectParser.cs | 58 --------- .../Objects/Legacy/Mania/ConvertHold.cs | 16 --- .../Objects/Legacy/Mania/ConvertSlider.cs | 15 --- .../Rulesets/Objects/Legacy/Osu/ConvertHit.cs | 20 --- .../Legacy/Osu/ConvertHitObjectParser.cs | 64 --------- .../Objects/Legacy/Osu/ConvertSlider.cs | 22 ---- .../Objects/Legacy/Osu/ConvertSpinner.cs | 24 ---- .../Objects/Legacy/Taiko/ConvertHit.cs | 12 -- .../Legacy/Taiko/ConvertHitObjectParser.cs | 51 -------- .../Objects/Legacy/Taiko/ConvertSlider.cs | 12 -- 22 files changed, 134 insertions(+), 522 deletions(-) delete mode 100644 osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHit.cs delete mode 100644 osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHitObjectParser.cs delete mode 100644 osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSlider.cs delete mode 100644 osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSpinner.cs create mode 100644 osu.Game/Rulesets/Objects/Legacy/ConvertHitCircle.cs rename osu.Game/Rulesets/Objects/Legacy/{Mania/ConvertSpinner.cs => ConvertHold.cs} (54%) rename osu.Game/Rulesets/Objects/Legacy/{Taiko => }/ConvertSpinner.cs (70%) delete mode 100644 osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHit.cs delete mode 100644 osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHitObjectParser.cs delete mode 100644 osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHold.cs delete mode 100644 osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSlider.cs delete mode 100644 osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHit.cs delete mode 100644 osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs delete mode 100644 osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSlider.cs delete mode 100644 osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSpinner.cs delete mode 100644 osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHit.cs delete mode 100644 osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHitObjectParser.cs delete mode 100644 osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSlider.cs diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index b068c87fbb..3d8c8a6e7a 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -38,8 +38,7 @@ namespace osu.Game.Beatmaps.Formats internal static RulesetStore? RulesetStore; private Beatmap beatmap = null!; - - private ConvertHitObjectParser? parser; + private ConvertHitObjectParser parser = null!; private LegacySampleBank defaultSampleBank; private int defaultSampleVolume = 100; @@ -80,6 +79,7 @@ namespace osu.Game.Beatmaps.Formats { this.beatmap = beatmap; this.beatmap.BeatmapInfo.BeatmapVersion = FormatVersion; + parser = new ConvertHitObjectParser(getOffsetTime(), FormatVersion); applyLegacyDefaults(this.beatmap.BeatmapInfo); @@ -162,7 +162,8 @@ namespace osu.Game.Beatmaps.Formats { if (hitObject is IHasRepeats hasRepeats) { - SampleControlPoint sampleControlPoint = (beatmap.ControlPointInfo as LegacyControlPointInfo)?.SamplePointAt(hitObject.StartTime + CONTROL_POINT_LENIENCY + 1) ?? SampleControlPoint.DEFAULT; + SampleControlPoint sampleControlPoint = (beatmap.ControlPointInfo as LegacyControlPointInfo)?.SamplePointAt(hitObject.StartTime + CONTROL_POINT_LENIENCY + 1) + ?? SampleControlPoint.DEFAULT; hitObject.Samples = hitObject.Samples.Select(o => sampleControlPoint.ApplyTo(o)).ToList(); for (int i = 0; i < hasRepeats.NodeSamples.Count; i++) @@ -175,7 +176,8 @@ namespace osu.Game.Beatmaps.Formats } else { - SampleControlPoint sampleControlPoint = (beatmap.ControlPointInfo as LegacyControlPointInfo)?.SamplePointAt(hitObject.GetEndTime() + CONTROL_POINT_LENIENCY) ?? SampleControlPoint.DEFAULT; + SampleControlPoint sampleControlPoint = (beatmap.ControlPointInfo as LegacyControlPointInfo)?.SamplePointAt(hitObject.GetEndTime() + CONTROL_POINT_LENIENCY) + ?? SampleControlPoint.DEFAULT; hitObject.Samples = hitObject.Samples.Select(o => sampleControlPoint.ApplyTo(o)).ToList(); } } @@ -263,29 +265,7 @@ namespace osu.Game.Beatmaps.Formats break; case @"Mode": - int rulesetID = Parsing.ParseInt(pair.Value); - - beatmap.BeatmapInfo.Ruleset = RulesetStore?.GetRuleset(rulesetID) ?? throw new ArgumentException("Ruleset is not available locally."); - - switch (rulesetID) - { - case 0: - parser = new Rulesets.Objects.Legacy.Osu.ConvertHitObjectParser(getOffsetTime(), FormatVersion); - break; - - case 1: - parser = new Rulesets.Objects.Legacy.Taiko.ConvertHitObjectParser(getOffsetTime(), FormatVersion); - break; - - case 2: - parser = new Rulesets.Objects.Legacy.Catch.ConvertHitObjectParser(getOffsetTime(), FormatVersion); - break; - - case 3: - parser = new Rulesets.Objects.Legacy.Mania.ConvertHitObjectParser(getOffsetTime(), FormatVersion); - break; - } - + beatmap.BeatmapInfo.Ruleset = RulesetStore?.GetRuleset(Parsing.ParseInt(pair.Value)) ?? throw new ArgumentException("Ruleset is not available locally."); break; case @"LetterboxInBreaks": @@ -617,17 +597,10 @@ namespace osu.Game.Beatmaps.Formats private void handleHitObject(string line) { - // If the ruleset wasn't specified, assume the osu!standard ruleset. - parser ??= new Rulesets.Objects.Legacy.Osu.ConvertHitObjectParser(getOffsetTime(), FormatVersion); - var obj = parser.Parse(line); + obj.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty); - if (obj != null) - { - obj.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty); - - beatmap.HitObjects.Add(obj); - } + beatmap.HitObjects.Add(obj); } private int getOffsetTime(int time) => time + (ApplyOffsets ? offset : 0); diff --git a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHit.cs b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHit.cs deleted file mode 100644 index 96c779e79b..0000000000 --- a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHit.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Rulesets.Objects.Types; -using osuTK; - -namespace osu.Game.Rulesets.Objects.Legacy.Catch -{ - /// - /// Legacy osu!catch Hit-type, used for parsing Beatmaps. - /// - internal sealed class ConvertHit : ConvertHitObject, IHasPosition - { - public float X => Position.X; - - public float Y => Position.Y; - - public Vector2 Position { get; set; } - } -} diff --git a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHitObjectParser.cs deleted file mode 100644 index a5c1a73fa7..0000000000 --- a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHitObjectParser.cs +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -#nullable disable - -using osuTK; -using osu.Game.Audio; -using System.Collections.Generic; - -namespace osu.Game.Rulesets.Objects.Legacy.Catch -{ - /// - /// A HitObjectParser to parse legacy osu!catch Beatmaps. - /// - public class ConvertHitObjectParser : Legacy.ConvertHitObjectParser - { - private ConvertHitObject lastObject; - - public ConvertHitObjectParser(double offset, int formatVersion) - : base(offset, formatVersion) - { - } - - protected override HitObject CreateHit(Vector2 position, bool newCombo, int comboOffset) - { - return lastObject = new ConvertHit - { - Position = position, - NewCombo = FirstObject || lastObject is ConvertSpinner || newCombo, - ComboOffset = newCombo ? comboOffset : 0 - }; - } - - protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, PathControlPoint[] controlPoints, double? length, int repeatCount, - IList> nodeSamples) - { - return lastObject = new ConvertSlider - { - Position = position, - NewCombo = FirstObject || lastObject is ConvertSpinner || newCombo, - ComboOffset = newCombo ? comboOffset : 0, - Path = new SliderPath(controlPoints, length), - NodeSamples = nodeSamples, - RepeatCount = repeatCount - }; - } - - protected override HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double duration) - { - return lastObject = new ConvertSpinner - { - Duration = duration, - NewCombo = newCombo - // Spinners cannot have combo offset. - }; - } - - protected override HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double duration) - { - return lastObject = null; - } - } -} diff --git a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSlider.cs deleted file mode 100644 index bcf1c7fae2..0000000000 --- a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSlider.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Rulesets.Objects.Types; -using osuTK; - -namespace osu.Game.Rulesets.Objects.Legacy.Catch -{ - /// - /// Legacy osu!catch Slider-type, used for parsing Beatmaps. - /// - internal sealed class ConvertSlider : Legacy.ConvertSlider, IHasPosition - { - public float X => Position.X; - - public float Y => Position.Y; - - public Vector2 Position { get; set; } - } -} diff --git a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSpinner.cs b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSpinner.cs deleted file mode 100644 index 5ef3d51cb3..0000000000 --- a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSpinner.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Rulesets.Objects.Types; - -namespace osu.Game.Rulesets.Objects.Legacy.Catch -{ - /// - /// Legacy osu!catch Spinner-type, used for parsing Beatmaps. - /// - internal sealed class ConvertSpinner : ConvertHitObject, IHasDuration, IHasXPosition - { - public double EndTime => StartTime + Duration; - - public double Duration { get; set; } - - public float X => 256; // Required for CatchBeatmapConverter - } -} diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitCircle.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitCircle.cs new file mode 100644 index 0000000000..d1852d7032 --- /dev/null +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitCircle.cs @@ -0,0 +1,13 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Rulesets.Objects.Legacy +{ + /// + /// Legacy "HitCircle" hit object type. + /// + /// + /// Only used for parsing beatmaps and not gameplay. + /// + internal sealed class ConvertHitCircle : ConvertHitObject; +} diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObject.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObject.cs index bb36aab0b3..d1b0d3c6c2 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObject.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObject.cs @@ -4,18 +4,28 @@ using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Scoring; +using osuTK; namespace osu.Game.Rulesets.Objects.Legacy { /// - /// A hit object only used for conversion, not actual gameplay. + /// Represents a legacy hit object. /// - internal abstract class ConvertHitObject : HitObject, IHasCombo + /// + /// Only used for parsing beatmaps and not gameplay. + /// + internal abstract class ConvertHitObject : HitObject, IHasCombo, IHasPosition { public bool NewCombo { get; set; } public int ComboOffset { get; set; } + public float X => Position.X; + + public float Y => Position.Y; + + public Vector2 Position { get; set; } + public override Judgement CreateJudgement() => new IgnoreJudgement(); protected override HitWindows CreateHitWindows() => HitWindows.Empty; diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index 89fd5f7384..646431c071 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osuTK; using osu.Game.Rulesets.Objects.Types; using System; @@ -11,7 +9,6 @@ using System.IO; using osu.Game.Beatmaps.Formats; using osu.Game.Audio; using System.Linq; -using JetBrains.Annotations; using osu.Framework.Utils; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.Legacy; @@ -24,24 +21,32 @@ namespace osu.Game.Rulesets.Objects.Legacy /// /// A HitObjectParser to parse legacy Beatmaps. /// - public abstract class ConvertHitObjectParser : HitObjectParser + public class ConvertHitObjectParser : HitObjectParser { /// /// The offset to apply to all time values. /// - protected readonly double Offset; + private readonly double offset; /// /// The .osu format (beatmap) version. /// - protected readonly int FormatVersion; + private readonly int formatVersion; - protected bool FirstObject { get; private set; } = true; + /// + /// Whether the current hitobject is the first hitobject in the beatmap. + /// + private bool firstObject = true; - protected ConvertHitObjectParser(double offset, int formatVersion) + /// + /// The last parsed hitobject. + /// + private ConvertHitObject? lastObject; + + internal ConvertHitObjectParser(double offset, int formatVersion) { - Offset = offset; - FormatVersion = formatVersion; + this.offset = offset; + this.formatVersion = formatVersion; } public override HitObject Parse(string text) @@ -49,11 +54,11 @@ namespace osu.Game.Rulesets.Objects.Legacy string[] split = text.Split(','); Vector2 pos = - FormatVersion >= LegacyBeatmapEncoder.FIRST_LAZER_VERSION + formatVersion >= LegacyBeatmapEncoder.FIRST_LAZER_VERSION ? new Vector2(Parsing.ParseFloat(split[0], Parsing.MAX_COORDINATE_VALUE), Parsing.ParseFloat(split[1], Parsing.MAX_COORDINATE_VALUE)) : new Vector2((int)Parsing.ParseFloat(split[0], Parsing.MAX_COORDINATE_VALUE), (int)Parsing.ParseFloat(split[1], Parsing.MAX_COORDINATE_VALUE)); - double startTime = Parsing.ParseDouble(split[2]) + Offset; + double startTime = Parsing.ParseDouble(split[2]) + offset; LegacyHitObjectType type = (LegacyHitObjectType)Parsing.ParseInt(split[3]); @@ -66,11 +71,11 @@ namespace osu.Game.Rulesets.Objects.Legacy var soundType = (LegacyHitSoundType)Parsing.ParseInt(split[4]); var bankInfo = new SampleBankInfo(); - HitObject result = null; + HitObject? result = null; if (type.HasFlag(LegacyHitObjectType.Circle)) { - result = CreateHit(pos, combo, comboOffset); + result = createHitCircle(pos, combo, comboOffset); if (split.Length > 5) readCustomSampleBanks(split[5], bankInfo); @@ -145,13 +150,13 @@ namespace osu.Game.Rulesets.Objects.Legacy for (int i = 0; i < nodes; i++) nodeSamples.Add(convertSoundType(nodeSoundTypes[i], nodeBankInfos[i])); - result = CreateSlider(pos, combo, comboOffset, convertPathString(split[5], pos), length, repeatCount, nodeSamples); + result = createSlider(pos, combo, comboOffset, convertPathString(split[5], pos), length, repeatCount, nodeSamples); } else if (type.HasFlag(LegacyHitObjectType.Spinner)) { - double duration = Math.Max(0, Parsing.ParseDouble(split[5]) + Offset - startTime); + double duration = Math.Max(0, Parsing.ParseDouble(split[5]) + offset - startTime); - result = CreateSpinner(new Vector2(512, 384) / 2, combo, comboOffset, duration); + result = createSpinner(new Vector2(512, 384) / 2, combo, comboOffset, duration); if (split.Length > 6) readCustomSampleBanks(split[6], bankInfo); @@ -169,7 +174,7 @@ namespace osu.Game.Rulesets.Objects.Legacy readCustomSampleBanks(string.Join(':', ss.Skip(1)), bankInfo); } - result = CreateHold(pos, combo, comboOffset, endTime + Offset - startTime); + result = createHold(pos, combo, comboOffset, endTime + offset - startTime); } if (result == null) @@ -180,7 +185,7 @@ namespace osu.Game.Rulesets.Objects.Legacy if (result.Samples.Count == 0) result.Samples = convertSoundType(soundType, bankInfo); - FirstObject = false; + firstObject = false; return result; } @@ -200,10 +205,11 @@ namespace osu.Game.Rulesets.Objects.Legacy if (!Enum.IsDefined(addBank)) addBank = LegacySampleBank.Normal; - string stringBank = bank.ToString().ToLowerInvariant(); + string? stringBank = bank.ToString().ToLowerInvariant(); + string? stringAddBank = addBank.ToString().ToLowerInvariant(); + if (stringBank == @"none") stringBank = null; - string stringAddBank = addBank.ToString().ToLowerInvariant(); if (stringAddBank == @"none") { @@ -357,7 +363,7 @@ namespace osu.Game.Rulesets.Objects.Legacy { int endPointLength = endPoint == null ? 0 : 1; - if (FormatVersion < LegacyBeatmapEncoder.FIRST_LAZER_VERSION) + if (formatVersion < LegacyBeatmapEncoder.FIRST_LAZER_VERSION) { if (vertices.Length + endPointLength != 3) type = PathType.BEZIER; @@ -393,7 +399,7 @@ namespace osu.Game.Rulesets.Objects.Legacy // Legacy CATMULL sliders don't support multiple segments, so adjacent CATMULL segments should be treated as a single one. // Importantly, this is not applied to the first control point, which may duplicate the slider path's position // resulting in a duplicate (0,0) control point in the resultant list. - if (type == PathType.CATMULL && endIndex > 1 && FormatVersion < LegacyBeatmapEncoder.FIRST_LAZER_VERSION) + if (type == PathType.CATMULL && endIndex > 1 && formatVersion < LegacyBeatmapEncoder.FIRST_LAZER_VERSION) continue; // The last control point of each segment is not allowed to start a new implicit segment. @@ -442,7 +448,15 @@ namespace osu.Game.Rulesets.Objects.Legacy /// Whether the hit object creates a new combo. /// When starting a new combo, the offset of the new combo relative to the current one. /// The hit object. - protected abstract HitObject CreateHit(Vector2 position, bool newCombo, int comboOffset); + private HitObject createHitCircle(Vector2 position, bool newCombo, int comboOffset) + { + return lastObject = new ConvertHitCircle + { + Position = position, + NewCombo = firstObject || lastObject is ConvertSpinner || newCombo, + ComboOffset = newCombo ? comboOffset : 0 + }; + } /// /// Creats a legacy Slider-type hit object. @@ -455,8 +469,19 @@ namespace osu.Game.Rulesets.Objects.Legacy /// The slider repeat count. /// The samples to be played when the slider nodes are hit. This includes the head and tail of the slider. /// The hit object. - protected abstract HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, PathControlPoint[] controlPoints, double? length, int repeatCount, - IList> nodeSamples); + private HitObject createSlider(Vector2 position, bool newCombo, int comboOffset, PathControlPoint[] controlPoints, double? length, int repeatCount, + IList> nodeSamples) + { + return lastObject = new ConvertSlider + { + Position = position, + NewCombo = firstObject || lastObject is ConvertSpinner || newCombo, + ComboOffset = newCombo ? comboOffset : 0, + Path = new SliderPath(controlPoints, length), + NodeSamples = nodeSamples, + RepeatCount = repeatCount + }; + } /// /// Creates a legacy Spinner-type hit object. @@ -466,7 +491,16 @@ namespace osu.Game.Rulesets.Objects.Legacy /// When starting a new combo, the offset of the new combo relative to the current one. /// The spinner duration. /// The hit object. - protected abstract HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double duration); + private HitObject createSpinner(Vector2 position, bool newCombo, int comboOffset, double duration) + { + return lastObject = new ConvertSpinner + { + Position = position, + Duration = duration, + NewCombo = newCombo + // Spinners cannot have combo offset. + }; + } /// /// Creates a legacy Hold-type hit object. @@ -475,7 +509,14 @@ namespace osu.Game.Rulesets.Objects.Legacy /// Whether the hit object creates a new combo. /// When starting a new combo, the offset of the new combo relative to the current one. /// The hold duration. - protected abstract HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double duration); + private HitObject createHold(Vector2 position, bool newCombo, int comboOffset, double duration) + { + return lastObject = new ConvertHold + { + Position = position, + Duration = duration + }; + } private List convertSoundType(LegacyHitSoundType type, SampleBankInfo bankInfo) { @@ -511,21 +552,19 @@ namespace osu.Game.Rulesets.Objects.Legacy /// /// An optional overriding filename which causes all bank/sample specifications to be ignored. /// - public string Filename; + public string? Filename; /// /// The bank identifier to use for the base ("hitnormal") sample. /// Transferred to when appropriate. /// - [CanBeNull] - public string BankForNormal; + public string? BankForNormal; /// /// The bank identifier to use for additions ("hitwhistle", "hitfinish", "hitclap"). /// Transferred to when appropriate. /// - [CanBeNull] - public string BankForAdditions; + public string? BankForAdditions; /// /// Hit sample volume (0-100). @@ -548,8 +587,6 @@ namespace osu.Game.Rulesets.Objects.Legacy public SampleBankInfo Clone() => (SampleBankInfo)MemberwiseClone(); } -#nullable enable - public class LegacyHitSampleInfo : HitSampleInfo, IEquatable { public readonly int CustomSampleBank; @@ -577,13 +614,14 @@ namespace osu.Game.Rulesets.Objects.Legacy IsLayered = isLayered; } - public sealed override HitSampleInfo With(Optional newName = default, Optional newBank = default, Optional newSuffix = default, Optional newVolume = default, Optional newEditorAutoBank = default) + public sealed override HitSampleInfo With(Optional newName = default, Optional newBank = default, Optional newSuffix = default, Optional newVolume = default, + Optional newEditorAutoBank = default) => With(newName, newBank, newVolume, newEditorAutoBank); - public virtual LegacyHitSampleInfo With(Optional newName = default, Optional newBank = default, Optional newVolume = default, Optional newEditorAutoBank = default, - Optional newCustomSampleBank = default, - Optional newIsLayered = default) - => new LegacyHitSampleInfo(newName.GetOr(Name), newBank.GetOr(Bank), newVolume.GetOr(Volume), newEditorAutoBank.GetOr(EditorAutoBank), newCustomSampleBank.GetOr(CustomSampleBank), newIsLayered.GetOr(IsLayered)); + public virtual LegacyHitSampleInfo With(Optional newName = default, Optional newBank = default, Optional newVolume = default, + Optional newEditorAutoBank = default, Optional newCustomSampleBank = default, Optional newIsLayered = default) + => new LegacyHitSampleInfo(newName.GetOr(Name), newBank.GetOr(Bank), newVolume.GetOr(Volume), newEditorAutoBank.GetOr(EditorAutoBank), newCustomSampleBank.GetOr(CustomSampleBank), + newIsLayered.GetOr(IsLayered)); public bool Equals(LegacyHitSampleInfo? other) // The additions to equality checks here are *required* to ensure that pooling works correctly. @@ -615,9 +653,8 @@ namespace osu.Game.Rulesets.Objects.Legacy Path.ChangeExtension(Filename, null) }; - public sealed override LegacyHitSampleInfo With(Optional newName = default, Optional newBank = default, Optional newVolume = default, Optional newEditorAutoBank = default, - Optional newCustomSampleBank = default, - Optional newIsLayered = default) + public sealed override LegacyHitSampleInfo With(Optional newName = default, Optional newBank = default, Optional newVolume = default, + Optional newEditorAutoBank = default, Optional newCustomSampleBank = default, Optional newIsLayered = default) => new FileHitSampleInfo(Filename, newVolume.GetOr(Volume)); public bool Equals(FileHitSampleInfo? other) diff --git a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSpinner.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHold.cs similarity index 54% rename from osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSpinner.cs rename to osu.Game/Rulesets/Objects/Legacy/ConvertHold.cs index c05aaceb9c..d74224892b 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSpinner.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHold.cs @@ -3,17 +3,18 @@ using osu.Game.Rulesets.Objects.Types; -namespace osu.Game.Rulesets.Objects.Legacy.Mania +namespace osu.Game.Rulesets.Objects.Legacy { /// - /// Legacy osu!mania Spinner-type, used for parsing Beatmaps. + /// Legacy "Hold" hit object type. Generally only valid in the mania ruleset. /// - internal sealed class ConvertSpinner : ConvertHitObject, IHasDuration, IHasXPosition + /// + /// Only used for parsing beatmaps and not gameplay. + /// + internal sealed class ConvertHold : ConvertHitObject, IHasDuration { public double Duration { get; set; } public double EndTime => StartTime + Duration; - - public float X { get; set; } } } diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs index 683eefa8f4..fee68f2f11 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Rulesets.Objects.Types; using System.Collections.Generic; using Newtonsoft.Json; @@ -13,7 +11,13 @@ using osu.Game.Beatmaps.ControlPoints; namespace osu.Game.Rulesets.Objects.Legacy { - internal abstract class ConvertSlider : ConvertHitObject, IHasPathWithRepeats, IHasSliderVelocity + /// + /// Legacy "Slider" hit object type. + /// + /// + /// Only used for parsing beatmaps and not gameplay. + /// + internal class ConvertSlider : ConvertHitObject, IHasPathWithRepeats, IHasSliderVelocity, IHasGenerateTicks { /// /// Scoring distance with a speed-adjusted beat length of 1 second. @@ -50,6 +54,8 @@ namespace osu.Game.Rulesets.Objects.Legacy set => SliderVelocityMultiplierBindable.Value = value; } + public bool GenerateTicks { get; set; } = true; + protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty) { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); diff --git a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSpinner.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertSpinner.cs similarity index 70% rename from osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSpinner.cs rename to osu.Game/Rulesets/Objects/Legacy/ConvertSpinner.cs index 1d5ecb1ef3..59551cd37a 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSpinner.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertSpinner.cs @@ -3,11 +3,14 @@ using osu.Game.Rulesets.Objects.Types; -namespace osu.Game.Rulesets.Objects.Legacy.Taiko +namespace osu.Game.Rulesets.Objects.Legacy { /// - /// Legacy osu!taiko Spinner-type, used for parsing Beatmaps. + /// Legacy "Spinner" hit object type. /// + /// + /// Only used for parsing beatmaps and not gameplay. + /// internal sealed class ConvertSpinner : ConvertHitObject, IHasDuration { public double Duration { get; set; } diff --git a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHit.cs b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHit.cs deleted file mode 100644 index 0b69817c13..0000000000 --- a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHit.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Rulesets.Objects.Types; - -namespace osu.Game.Rulesets.Objects.Legacy.Mania -{ - /// - /// Legacy osu!mania Hit-type, used for parsing Beatmaps. - /// - internal sealed class ConvertHit : ConvertHitObject, IHasXPosition - { - public float X { get; set; } - } -} diff --git a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHitObjectParser.cs deleted file mode 100644 index 386eb8d3ee..0000000000 --- a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHitObjectParser.cs +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osuTK; -using osu.Game.Audio; -using System.Collections.Generic; - -namespace osu.Game.Rulesets.Objects.Legacy.Mania -{ - /// - /// A HitObjectParser to parse legacy osu!mania Beatmaps. - /// - public class ConvertHitObjectParser : Legacy.ConvertHitObjectParser - { - public ConvertHitObjectParser(double offset, int formatVersion) - : base(offset, formatVersion) - { - } - - protected override HitObject CreateHit(Vector2 position, bool newCombo, int comboOffset) - { - return new ConvertHit - { - X = position.X - }; - } - - protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, PathControlPoint[] controlPoints, double? length, int repeatCount, - IList> nodeSamples) - { - return new ConvertSlider - { - X = position.X, - Path = new SliderPath(controlPoints, length), - NodeSamples = nodeSamples, - RepeatCount = repeatCount - }; - } - - protected override HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double duration) - { - return new ConvertSpinner - { - X = position.X, - Duration = duration - }; - } - - protected override HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double duration) - { - return new ConvertHold - { - X = position.X, - Duration = duration - }; - } - } -} diff --git a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHold.cs b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHold.cs deleted file mode 100644 index 2fa4766c1d..0000000000 --- a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHold.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Rulesets.Objects.Types; - -namespace osu.Game.Rulesets.Objects.Legacy.Mania -{ - internal sealed class ConvertHold : ConvertHitObject, IHasXPosition, IHasDuration - { - public float X { get; set; } - - public double Duration { get; set; } - - public double EndTime => StartTime + Duration; - } -} diff --git a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSlider.cs deleted file mode 100644 index 84cde5fa95..0000000000 --- a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSlider.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Rulesets.Objects.Types; - -namespace osu.Game.Rulesets.Objects.Legacy.Mania -{ - /// - /// Legacy osu!mania Slider-type, used for parsing Beatmaps. - /// - internal sealed class ConvertSlider : Legacy.ConvertSlider, IHasXPosition - { - public float X { get; set; } - } -} diff --git a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHit.cs b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHit.cs deleted file mode 100644 index b7cd4b0dcc..0000000000 --- a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHit.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Rulesets.Objects.Types; -using osuTK; - -namespace osu.Game.Rulesets.Objects.Legacy.Osu -{ - /// - /// Legacy osu! Hit-type, used for parsing Beatmaps. - /// - internal sealed class ConvertHit : ConvertHitObject, IHasPosition - { - public Vector2 Position { get; set; } - - public float X => Position.X; - - public float Y => Position.Y; - } -} diff --git a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs deleted file mode 100644 index 43c346b621..0000000000 --- a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -#nullable disable - -using osuTK; -using System.Collections.Generic; -using osu.Game.Audio; - -namespace osu.Game.Rulesets.Objects.Legacy.Osu -{ - /// - /// A HitObjectParser to parse legacy osu! Beatmaps. - /// - public class ConvertHitObjectParser : Legacy.ConvertHitObjectParser - { - private ConvertHitObject lastObject; - - public ConvertHitObjectParser(double offset, int formatVersion) - : base(offset, formatVersion) - { - } - - protected override HitObject CreateHit(Vector2 position, bool newCombo, int comboOffset) - { - return lastObject = new ConvertHit - { - Position = position, - NewCombo = FirstObject || lastObject is ConvertSpinner || newCombo, - ComboOffset = newCombo ? comboOffset : 0 - }; - } - - protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, PathControlPoint[] controlPoints, double? length, int repeatCount, - IList> nodeSamples) - { - return lastObject = new ConvertSlider - { - Position = position, - NewCombo = FirstObject || lastObject is ConvertSpinner || newCombo, - ComboOffset = newCombo ? comboOffset : 0, - Path = new SliderPath(controlPoints, length), - NodeSamples = nodeSamples, - RepeatCount = repeatCount - }; - } - - protected override HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double duration) - { - return lastObject = new ConvertSpinner - { - Position = position, - Duration = duration, - NewCombo = newCombo - // Spinners cannot have combo offset. - }; - } - - protected override HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double duration) - { - return lastObject = null; - } - } -} diff --git a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSlider.cs deleted file mode 100644 index 8c37154f95..0000000000 --- a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSlider.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Rulesets.Objects.Types; -using osuTK; - -namespace osu.Game.Rulesets.Objects.Legacy.Osu -{ - /// - /// Legacy osu! Slider-type, used for parsing Beatmaps. - /// - internal sealed class ConvertSlider : Legacy.ConvertSlider, IHasPosition, IHasGenerateTicks - { - public Vector2 Position { get; set; } - - public float X => Position.X; - - public float Y => Position.Y; - - public bool GenerateTicks { get; set; } = true; - } -} diff --git a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSpinner.cs b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSpinner.cs deleted file mode 100644 index d6e24b6bbf..0000000000 --- a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSpinner.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Rulesets.Objects.Types; -using osuTK; - -namespace osu.Game.Rulesets.Objects.Legacy.Osu -{ - /// - /// Legacy osu! Spinner-type, used for parsing Beatmaps. - /// - internal sealed class ConvertSpinner : ConvertHitObject, IHasDuration, IHasPosition - { - public double Duration { get; set; } - - public double EndTime => StartTime + Duration; - - public Vector2 Position { get; set; } - - public float X => Position.X; - - public float Y => Position.Y; - } -} diff --git a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHit.cs b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHit.cs deleted file mode 100644 index cb5178ce48..0000000000 --- a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHit.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -namespace osu.Game.Rulesets.Objects.Legacy.Taiko -{ - /// - /// Legacy osu!taiko Hit-type, used for parsing Beatmaps. - /// - internal sealed class ConvertHit : ConvertHitObject - { - } -} diff --git a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHitObjectParser.cs deleted file mode 100644 index d62e8cd04c..0000000000 --- a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHitObjectParser.cs +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -#nullable disable - -using osuTK; -using System.Collections.Generic; -using osu.Game.Audio; - -namespace osu.Game.Rulesets.Objects.Legacy.Taiko -{ - /// - /// A HitObjectParser to parse legacy osu!taiko Beatmaps. - /// - public class ConvertHitObjectParser : Legacy.ConvertHitObjectParser - { - public ConvertHitObjectParser(double offset, int formatVersion) - : base(offset, formatVersion) - { - } - - protected override HitObject CreateHit(Vector2 position, bool newCombo, int comboOffset) - { - return new ConvertHit(); - } - - protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, PathControlPoint[] controlPoints, double? length, int repeatCount, - IList> nodeSamples) - { - return new ConvertSlider - { - Path = new SliderPath(controlPoints, length), - NodeSamples = nodeSamples, - RepeatCount = repeatCount - }; - } - - protected override HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double duration) - { - return new ConvertSpinner - { - Duration = duration - }; - } - - protected override HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double duration) - { - return null; - } - } -} diff --git a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSlider.cs deleted file mode 100644 index 821554f7ee..0000000000 --- a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSlider.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -namespace osu.Game.Rulesets.Objects.Legacy.Taiko -{ - /// - /// Legacy osu!taiko Slider-type, used for parsing Beatmaps. - /// - internal sealed class ConvertSlider : Legacy.ConvertSlider - { - } -} From 7206e97b7beb637c411a39f0418c50db0f89c427 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 11 Nov 2024 15:23:23 +0900 Subject: [PATCH 19/23] Add `IHasLegacyHitObjectType` to ConvertHitObject --- .../Beatmaps/Legacy/LegacyHitObjectType.cs | 2 +- .../Objects/Legacy/ConvertHitObject.cs | 5 ++++- .../Objects/Legacy/ConvertHitObjectParser.cs | 13 +++++++------ .../Objects/Legacy/IHasLegacyHitObjectType.cs | 18 ++++++++++++++++++ 4 files changed, 30 insertions(+), 8 deletions(-) create mode 100644 osu.Game/Rulesets/Objects/Legacy/IHasLegacyHitObjectType.cs diff --git a/osu.Game/Beatmaps/Legacy/LegacyHitObjectType.cs b/osu.Game/Beatmaps/Legacy/LegacyHitObjectType.cs index 07f170f996..6fab66bf70 100644 --- a/osu.Game/Beatmaps/Legacy/LegacyHitObjectType.cs +++ b/osu.Game/Beatmaps/Legacy/LegacyHitObjectType.cs @@ -6,7 +6,7 @@ using System; namespace osu.Game.Beatmaps.Legacy { [Flags] - internal enum LegacyHitObjectType + public enum LegacyHitObjectType { Circle = 1, Slider = 1 << 1, diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObject.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObject.cs index d1b0d3c6c2..28683583ee 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObject.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObject.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Game.Beatmaps.Legacy; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Scoring; @@ -14,7 +15,7 @@ namespace osu.Game.Rulesets.Objects.Legacy /// /// Only used for parsing beatmaps and not gameplay. /// - internal abstract class ConvertHitObject : HitObject, IHasCombo, IHasPosition + internal abstract class ConvertHitObject : HitObject, IHasCombo, IHasPosition, IHasLegacyHitObjectType { public bool NewCombo { get; set; } @@ -26,6 +27,8 @@ namespace osu.Game.Rulesets.Objects.Legacy public Vector2 Position { get; set; } + public LegacyHitObjectType LegacyType { get; set; } + public override Judgement CreateJudgement() => new IgnoreJudgement(); protected override HitWindows CreateHitWindows() => HitWindows.Empty; diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index 646431c071..1c933392c0 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -71,7 +71,7 @@ namespace osu.Game.Rulesets.Objects.Legacy var soundType = (LegacyHitSoundType)Parsing.ParseInt(split[4]); var bankInfo = new SampleBankInfo(); - HitObject? result = null; + ConvertHitObject? result = null; if (type.HasFlag(LegacyHitObjectType.Circle)) { @@ -181,6 +181,7 @@ namespace osu.Game.Rulesets.Objects.Legacy throw new InvalidDataException($"Unknown hit object type: {split[3]}"); result.StartTime = startTime; + result.LegacyType = type; if (result.Samples.Count == 0) result.Samples = convertSoundType(soundType, bankInfo); @@ -448,7 +449,7 @@ namespace osu.Game.Rulesets.Objects.Legacy /// Whether the hit object creates a new combo. /// When starting a new combo, the offset of the new combo relative to the current one. /// The hit object. - private HitObject createHitCircle(Vector2 position, bool newCombo, int comboOffset) + private ConvertHitObject createHitCircle(Vector2 position, bool newCombo, int comboOffset) { return lastObject = new ConvertHitCircle { @@ -469,8 +470,8 @@ namespace osu.Game.Rulesets.Objects.Legacy /// The slider repeat count. /// The samples to be played when the slider nodes are hit. This includes the head and tail of the slider. /// The hit object. - private HitObject createSlider(Vector2 position, bool newCombo, int comboOffset, PathControlPoint[] controlPoints, double? length, int repeatCount, - IList> nodeSamples) + private ConvertHitObject createSlider(Vector2 position, bool newCombo, int comboOffset, PathControlPoint[] controlPoints, double? length, int repeatCount, + IList> nodeSamples) { return lastObject = new ConvertSlider { @@ -491,7 +492,7 @@ namespace osu.Game.Rulesets.Objects.Legacy /// When starting a new combo, the offset of the new combo relative to the current one. /// The spinner duration. /// The hit object. - private HitObject createSpinner(Vector2 position, bool newCombo, int comboOffset, double duration) + private ConvertHitObject createSpinner(Vector2 position, bool newCombo, int comboOffset, double duration) { return lastObject = new ConvertSpinner { @@ -509,7 +510,7 @@ namespace osu.Game.Rulesets.Objects.Legacy /// Whether the hit object creates a new combo. /// When starting a new combo, the offset of the new combo relative to the current one. /// The hold duration. - private HitObject createHold(Vector2 position, bool newCombo, int comboOffset, double duration) + private ConvertHitObject createHold(Vector2 position, bool newCombo, int comboOffset, double duration) { return lastObject = new ConvertHold { diff --git a/osu.Game/Rulesets/Objects/Legacy/IHasLegacyHitObjectType.cs b/osu.Game/Rulesets/Objects/Legacy/IHasLegacyHitObjectType.cs new file mode 100644 index 0000000000..71af57700d --- /dev/null +++ b/osu.Game/Rulesets/Objects/Legacy/IHasLegacyHitObjectType.cs @@ -0,0 +1,18 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Beatmaps.Legacy; + +namespace osu.Game.Rulesets.Objects.Legacy +{ + /// + /// A hit object from a legacy beatmap representation. + /// + public interface IHasLegacyHitObjectType + { + /// + /// The hit object type. + /// + LegacyHitObjectType LegacyType { get; } + } +} From 8c68db0a369bbfafe726accf1ff23ff902d3c6ed Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 11 Nov 2024 15:27:45 +0900 Subject: [PATCH 20/23] Remove unused params --- .../Rulesets/Objects/Legacy/ConvertHitObjectParser.cs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index 1c933392c0..f8bc0ce112 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -156,7 +156,7 @@ namespace osu.Game.Rulesets.Objects.Legacy { double duration = Math.Max(0, Parsing.ParseDouble(split[5]) + offset - startTime); - result = createSpinner(new Vector2(512, 384) / 2, combo, comboOffset, duration); + result = createSpinner(new Vector2(512, 384) / 2, combo, duration); if (split.Length > 6) readCustomSampleBanks(split[6], bankInfo); @@ -174,7 +174,7 @@ namespace osu.Game.Rulesets.Objects.Legacy readCustomSampleBanks(string.Join(':', ss.Skip(1)), bankInfo); } - result = createHold(pos, combo, comboOffset, endTime + offset - startTime); + result = createHold(pos, endTime + offset - startTime); } if (result == null) @@ -489,10 +489,9 @@ namespace osu.Game.Rulesets.Objects.Legacy /// /// The position of the hit object. /// Whether the hit object creates a new combo. - /// When starting a new combo, the offset of the new combo relative to the current one. /// The spinner duration. /// The hit object. - private ConvertHitObject createSpinner(Vector2 position, bool newCombo, int comboOffset, double duration) + private ConvertHitObject createSpinner(Vector2 position, bool newCombo, double duration) { return lastObject = new ConvertSpinner { @@ -507,10 +506,8 @@ namespace osu.Game.Rulesets.Objects.Legacy /// Creates a legacy Hold-type hit object. /// /// The position of the hit object. - /// Whether the hit object creates a new combo. - /// When starting a new combo, the offset of the new combo relative to the current one. /// The hold duration. - private ConvertHitObject createHold(Vector2 position, bool newCombo, int comboOffset, double duration) + private ConvertHitObject createHold(Vector2 position, double duration) { return lastObject = new ConvertHold { From 06380f91fca4656f96a3c32c82960d3c55aab9bc Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 11 Nov 2024 16:25:05 +0900 Subject: [PATCH 21/23] Update test --- .../SongSelect/TestSceneBeatmapInfoWedge.cs | 38 +++++++------------ 1 file changed, 13 insertions(+), 25 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs index fd102da026..d8573b2d03 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs @@ -1,12 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Linq; -using JetBrains.Annotations; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -23,7 +20,6 @@ using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Legacy; -using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Taiko; @@ -35,15 +31,11 @@ namespace osu.Game.Tests.Visual.SongSelect [TestFixture] public partial class TestSceneBeatmapInfoWedge : OsuTestScene { - private RulesetStore rulesets; - private TestBeatmapInfoWedge infoWedge; - private readonly List beatmaps = new List(); + [Resolved] + private RulesetStore rulesets { get; set; } = null!; - [BackgroundDependencyLoader] - private void load(RulesetStore rulesets) - { - this.rulesets = rulesets; - } + private TestBeatmapInfoWedge infoWedge = null!; + private readonly List beatmaps = new List(); protected override void LoadComplete() { @@ -156,7 +148,7 @@ namespace osu.Game.Tests.Visual.SongSelect IBeatmap beatmap = createTestBeatmap(new OsuRuleset().RulesetInfo); beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 60 * 1000 / bpm }); - OsuModDoubleTime doubleTime = null; + OsuModDoubleTime doubleTime = null!; selectBeatmap(beatmap); checkDisplayedBPM($"{bpm}"); @@ -173,7 +165,7 @@ namespace osu.Game.Tests.Visual.SongSelect [TestCase(120, 120.4, null, "120")] [TestCase(120, 120.6, "DT", "180-182 (mostly 180)")] [TestCase(120, 120.4, "DT", "180")] - public void TestVaryingBPM(double commonBpm, double otherBpm, string mod, string expectedDisplay) + public void TestVaryingBPM(double commonBpm, double otherBpm, string? mod, string expectedDisplay) { IBeatmap beatmap = createTestBeatmap(new OsuRuleset().RulesetInfo); beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 60 * 1000 / commonBpm }); @@ -203,7 +195,7 @@ namespace osu.Game.Tests.Visual.SongSelect double drain = beatmap.CalculateDrainLength(); beatmap.BeatmapInfo.Length = drain; - OsuModDoubleTime doubleTime = null; + OsuModDoubleTime doubleTime = null!; selectBeatmap(beatmap); checkDisplayedLength(drain); @@ -221,14 +213,15 @@ namespace osu.Game.Tests.Visual.SongSelect AddUntilStep($"check map drain ({displayedLength})", () => { - var label = infoWedge.DisplayedContent.ChildrenOfType().Single(l => l.Statistic.Name == BeatmapsetsStrings.ShowStatsTotalLength(displayedLength)); + var label = infoWedge.DisplayedContent.ChildrenOfType() + .Single(l => l.Statistic.Name == BeatmapsetsStrings.ShowStatsTotalLength(displayedLength)); return label.Statistic.Content == displayedLength.ToString(); }); } private void setRuleset(RulesetInfo rulesetInfo) { - Container containerBefore = null; + Container? containerBefore = null; AddStep("set ruleset", () => { @@ -242,9 +235,9 @@ namespace osu.Game.Tests.Visual.SongSelect AddUntilStep("wait for async load", () => infoWedge.DisplayedContent != containerBefore); } - private void selectBeatmap([CanBeNull] IBeatmap b) + private void selectBeatmap(IBeatmap? b) { - Container containerBefore = null; + Container? containerBefore = null; AddStep($"select {b?.Metadata.Title ?? "null"} beatmap", () => { @@ -307,11 +300,6 @@ namespace osu.Game.Tests.Visual.SongSelect public new WedgeInfoText Info => base.Info; } - private class TestHitObject : ConvertHitObject, IHasPosition - { - public float X => 0; - public float Y => 0; - public Vector2 Position { get; } = Vector2.Zero; - } + private class TestHitObject : ConvertHitObject; } } From 1c3a30a2976a19fb6aa099281919e871cf7c6ca1 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 11 Nov 2024 16:30:26 +0900 Subject: [PATCH 22/23] Fix the other test class too --- .../Visual/SongSelectV2/TestSceneBeatmapInfoWedge.cs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapInfoWedge.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapInfoWedge.cs index 35bd4ee958..fbbab3a604 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapInfoWedge.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapInfoWedge.cs @@ -14,9 +14,7 @@ using osu.Game.Beatmaps; using osu.Game.Rulesets; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Legacy; -using osu.Game.Rulesets.Objects.Types; using osu.Game.Screens.Select; -using osuTK; namespace osu.Game.Tests.Visual.SongSelectV2 { @@ -209,11 +207,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2 public new WedgeInfoText? Info => base.Info; } - private class TestHitObject : ConvertHitObject, IHasPosition - { - public float X => 0; - public float Y => 0; - public Vector2 Position { get; } = Vector2.Zero; - } + private class TestHitObject : ConvertHitObject; } } From d37c1bb6d04269f32d9739dcc74fb781bb937358 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 Nov 2024 13:37:53 +0900 Subject: [PATCH 23/23] Remove redundant null initialisation and apply nullability --- osu.Game/Beatmaps/StarDifficulty.cs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/osu.Game/Beatmaps/StarDifficulty.cs b/osu.Game/Beatmaps/StarDifficulty.cs index 51358fcd7f..9f7a92fe46 100644 --- a/osu.Game/Beatmaps/StarDifficulty.cs +++ b/osu.Game/Beatmaps/StarDifficulty.cs @@ -1,9 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - -using JetBrains.Annotations; using osu.Framework.Utils; using osu.Game.Rulesets.Difficulty; @@ -25,20 +22,18 @@ namespace osu.Game.Beatmaps /// The difficulty attributes computed for the given beatmap. /// Might not be available if the star difficulty is associated with a beatmap that's not locally available. /// - [CanBeNull] - public readonly DifficultyAttributes DifficultyAttributes; + public readonly DifficultyAttributes? DifficultyAttributes; /// /// The performance attributes computed for a perfect score on the given beatmap. /// Might not be available if the star difficulty is associated with a beatmap that's not locally available. /// - [CanBeNull] - public readonly PerformanceAttributes PerformanceAttributes; + public readonly PerformanceAttributes? PerformanceAttributes; /// /// Creates a structure. /// - public StarDifficulty([NotNull] DifficultyAttributes difficulty, [NotNull] PerformanceAttributes performance) + public StarDifficulty(DifficultyAttributes difficulty, PerformanceAttributes performance) { Stars = double.IsFinite(difficulty.StarRating) ? difficulty.StarRating : 0; MaxCombo = difficulty.MaxCombo; @@ -55,7 +50,6 @@ namespace osu.Game.Beatmaps { Stars = double.IsFinite(starDifficulty) ? starDifficulty : 0; MaxCombo = maxCombo; - DifficultyAttributes = null; } public DifficultyRating DifficultyRating => GetDifficultyRating(Stars);