From 579d5b51eb25e90e7bc28f284b28c5edc14fc249 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 19 Oct 2022 20:34:41 +0900 Subject: [PATCH 01/16] Add and consume sample bank constants --- .../TestSceneAutoJuiceStream.cs | 2 +- .../Editor/TestSceneSliderSplitting.cs | 4 +- .../Formats/LegacyBeatmapDecoderTest.cs | 14 +++--- ...estSceneHitObjectSamplePointAdjustments.cs | 45 ++++++++++--------- .../TestSceneGameplaySampleTriggerSource.cs | 4 +- osu.Game/Audio/HitSampleInfo.cs | 9 ++++ .../ControlPoints/SampleControlPoint.cs | 4 +- .../Beatmaps/Formats/LegacyBeatmapDecoder.cs | 3 +- .../Beatmaps/Formats/LegacyBeatmapEncoder.cs | 6 +-- 9 files changed, 51 insertions(+), 40 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs index 202228c9e7..e47a687f24 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs @@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Catch.Tests NewCombo = i % 8 == 0, Samples = new List(new[] { - new HitSampleInfo(HitSampleInfo.HIT_NORMAL, "normal", volume: 100) + new HitSampleInfo(HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_NORMAL, volume: 100) }) }); } diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs index 015952c59a..d642d6a5ed 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs @@ -181,7 +181,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { if (slider is null) return; - slider.SampleControlPoint.SampleBank = "soft"; + slider.SampleControlPoint.SampleBank = HitSampleInfo.BANK_SOFT; slider.SampleControlPoint.SampleVolume = 70; sample = new HitSampleInfo("hitwhistle"); slider.Samples.Add(sample); @@ -207,7 +207,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddAssert("sliders have hitsounds", hasHitsounds); bool hasHitsounds() => sample is not null && - EditorBeatmap.HitObjects.All(o => o.SampleControlPoint.SampleBank == "soft" && + EditorBeatmap.HitObjects.All(o => o.SampleControlPoint.SampleBank == HitSampleInfo.BANK_SOFT && o.SampleControlPoint.SampleVolume == 70 && o.Samples.Contains(sample)); } diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index fdd0167ed3..d9817802f3 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -206,17 +206,17 @@ namespace osu.Game.Tests.Beatmaps.Formats var soundPoint = controlPoints.SamplePointAt(0); Assert.AreEqual(956, soundPoint.Time); - Assert.AreEqual("soft", soundPoint.SampleBank); + Assert.AreEqual(HitSampleInfo.BANK_SOFT, soundPoint.SampleBank); Assert.AreEqual(60, soundPoint.SampleVolume); soundPoint = controlPoints.SamplePointAt(53373); Assert.AreEqual(53373, soundPoint.Time); - Assert.AreEqual("soft", soundPoint.SampleBank); + Assert.AreEqual(HitSampleInfo.BANK_SOFT, soundPoint.SampleBank); Assert.AreEqual(60, soundPoint.SampleVolume); soundPoint = controlPoints.SamplePointAt(119637); Assert.AreEqual(119637, soundPoint.Time); - Assert.AreEqual("soft", soundPoint.SampleBank); + Assert.AreEqual(HitSampleInfo.BANK_SOFT, soundPoint.SampleBank); Assert.AreEqual(80, soundPoint.SampleVolume); var effectPoint = controlPoints.EffectPointAt(0); @@ -261,10 +261,10 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.That(controlPoints.EffectPointAt(2500).KiaiMode, Is.False); Assert.That(controlPoints.EffectPointAt(3500).KiaiMode, Is.True); - Assert.That(controlPoints.SamplePointAt(500).SampleBank, Is.EqualTo("drum")); - Assert.That(controlPoints.SamplePointAt(1500).SampleBank, Is.EqualTo("drum")); - Assert.That(controlPoints.SamplePointAt(2500).SampleBank, Is.EqualTo("normal")); - Assert.That(controlPoints.SamplePointAt(3500).SampleBank, Is.EqualTo("drum")); + Assert.That(controlPoints.SamplePointAt(500).SampleBank, Is.EqualTo(HitSampleInfo.BANK_DRUM)); + Assert.That(controlPoints.SamplePointAt(1500).SampleBank, Is.EqualTo(HitSampleInfo.BANK_DRUM)); + Assert.That(controlPoints.SamplePointAt(2500).SampleBank, Is.EqualTo(HitSampleInfo.BANK_NORMAL)); + Assert.That(controlPoints.SamplePointAt(3500).SampleBank, Is.EqualTo(HitSampleInfo.BANK_DRUM)); Assert.That(controlPoints.TimingPointAt(500).BeatLength, Is.EqualTo(500).Within(0.1)); Assert.That(controlPoints.TimingPointAt(1500).BeatLength, Is.EqualTo(500).Within(0.1)); diff --git a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSamplePointAdjustments.cs b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSamplePointAdjustments.cs index 6313842dfd..31939f6971 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSamplePointAdjustments.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSamplePointAdjustments.cs @@ -7,6 +7,7 @@ using System.Linq; using Humanizer; using NUnit.Framework; using osu.Framework.Testing; +using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics.UserInterface; @@ -41,7 +42,7 @@ namespace osu.Game.Tests.Visual.Editing Position = (OsuPlayfield.BASE_SIZE - new Vector2(100, 0)) / 2, SampleControlPoint = new SampleControlPoint { - SampleBank = "normal", + SampleBank = HitSampleInfo.BANK_NORMAL, SampleVolume = 80 } }); @@ -52,7 +53,7 @@ namespace osu.Game.Tests.Visual.Editing Position = (OsuPlayfield.BASE_SIZE + new Vector2(100, 0)) / 2, SampleControlPoint = new SampleControlPoint { - SampleBank = "soft", + SampleBank = HitSampleInfo.BANK_SOFT, SampleVolume = 60 } }); @@ -70,7 +71,7 @@ namespace osu.Game.Tests.Visual.Editing public void TestSingleSelection() { clickSamplePiece(0); - samplePopoverHasSingleBank("normal"); + samplePopoverHasSingleBank(HitSampleInfo.BANK_NORMAL); samplePopoverHasSingleVolume(80); dismissPopover(); @@ -80,14 +81,14 @@ namespace osu.Game.Tests.Visual.Editing AddStep("select first object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.First())); clickSamplePiece(1); - samplePopoverHasSingleBank("soft"); + samplePopoverHasSingleBank(HitSampleInfo.BANK_SOFT); samplePopoverHasSingleVolume(60); setVolumeViaPopover(90); hitObjectHasSampleVolume(1, 90); - setBankViaPopover("drum"); - hitObjectHasSampleBank(1, "drum"); + setBankViaPopover(HitSampleInfo.BANK_DRUM); + hitObjectHasSampleBank(1, HitSampleInfo.BANK_DRUM); } [Test] @@ -136,27 +137,27 @@ namespace osu.Game.Tests.Visual.Editing AddStep("unify sample bank", () => { foreach (var h in EditorBeatmap.HitObjects) - h.SampleControlPoint.SampleBank = "soft"; + h.SampleControlPoint.SampleBank = HitSampleInfo.BANK_SOFT; }); AddStep("select both objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects)); clickSamplePiece(0); - samplePopoverHasSingleBank("soft"); + samplePopoverHasSingleBank(HitSampleInfo.BANK_SOFT); dismissPopover(); clickSamplePiece(1); - samplePopoverHasSingleBank("soft"); + samplePopoverHasSingleBank(HitSampleInfo.BANK_SOFT); setBankViaPopover(string.Empty); - hitObjectHasSampleBank(0, "soft"); - hitObjectHasSampleBank(1, "soft"); - samplePopoverHasSingleBank("soft"); + hitObjectHasSampleBank(0, HitSampleInfo.BANK_SOFT); + hitObjectHasSampleBank(1, HitSampleInfo.BANK_SOFT); + samplePopoverHasSingleBank(HitSampleInfo.BANK_SOFT); - setBankViaPopover("drum"); - hitObjectHasSampleBank(0, "drum"); - hitObjectHasSampleBank(1, "drum"); - samplePopoverHasSingleBank("drum"); + setBankViaPopover(HitSampleInfo.BANK_DRUM); + hitObjectHasSampleBank(0, HitSampleInfo.BANK_DRUM); + hitObjectHasSampleBank(1, HitSampleInfo.BANK_DRUM); + samplePopoverHasSingleBank(HitSampleInfo.BANK_DRUM); } [Test] @@ -172,14 +173,14 @@ namespace osu.Game.Tests.Visual.Editing samplePopoverHasIndeterminateBank(); setBankViaPopover(string.Empty); - hitObjectHasSampleBank(0, "normal"); - hitObjectHasSampleBank(1, "soft"); + hitObjectHasSampleBank(0, HitSampleInfo.BANK_NORMAL); + hitObjectHasSampleBank(1, HitSampleInfo.BANK_SOFT); samplePopoverHasIndeterminateBank(); - setBankViaPopover("normal"); - hitObjectHasSampleBank(0, "normal"); - hitObjectHasSampleBank(1, "normal"); - samplePopoverHasSingleBank("normal"); + setBankViaPopover(HitSampleInfo.BANK_NORMAL); + hitObjectHasSampleBank(0, HitSampleInfo.BANK_NORMAL); + hitObjectHasSampleBank(1, HitSampleInfo.BANK_NORMAL); + samplePopoverHasSingleBank(HitSampleInfo.BANK_NORMAL); } private void clickSamplePiece(int objectIndex) => AddStep($"click {objectIndex.ToOrdinalWords()} sample piece", () => diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs index b6da562bd0..6e53302624 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs @@ -57,13 +57,13 @@ namespace osu.Game.Tests.Visual.Gameplay { StartTime = t += spacing, Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }, - SampleControlPoint = new SampleControlPoint { SampleBank = "soft" }, + SampleControlPoint = new SampleControlPoint { SampleBank = HitSampleInfo.BANK_SOFT }, }, new HitCircle { StartTime = t + spacing, Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_WHISTLE) }, - SampleControlPoint = new SampleControlPoint { SampleBank = "soft" }, + SampleControlPoint = new SampleControlPoint { SampleBank = HitSampleInfo.BANK_SOFT }, }, }); diff --git a/osu.Game/Audio/HitSampleInfo.cs b/osu.Game/Audio/HitSampleInfo.cs index efa5562cb8..81cfed23f5 100644 --- a/osu.Game/Audio/HitSampleInfo.cs +++ b/osu.Game/Audio/HitSampleInfo.cs @@ -19,11 +19,20 @@ namespace osu.Game.Audio public const string HIT_FINISH = @"hitfinish"; public const string HIT_CLAP = @"hitclap"; + public const string BANK_NORMAL = @"normal"; + public const string BANK_SOFT = @"soft"; + public const string BANK_DRUM = @"drum"; + /// /// All valid sample addition constants. /// public static IEnumerable AllAdditions => new[] { HIT_WHISTLE, HIT_FINISH, HIT_CLAP }; + /// + /// All valid bank constants. + /// + public static IEnumerable AllBanks => new[] { BANK_NORMAL, BANK_SOFT, BANK_DRUM }; + /// /// The name of the sample to load. /// diff --git a/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs index c454439c5c..e7b869cfa7 100644 --- a/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs @@ -14,7 +14,7 @@ namespace osu.Game.Beatmaps.ControlPoints /// public class SampleControlPoint : ControlPoint, IEquatable { - public const string DEFAULT_BANK = "normal"; + public const string DEFAULT_BANK = HitSampleInfo.BANK_NORMAL; public static readonly SampleControlPoint DEFAULT = new SampleControlPoint { @@ -30,7 +30,7 @@ namespace osu.Game.Beatmaps.ControlPoints public readonly Bindable SampleBankBindable = new Bindable(DEFAULT_BANK) { Default = DEFAULT_BANK }; /// - /// The speed multiplier at this control point. + /// The sample bank at this control point. /// public string SampleBank { diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index 75500fbc4e..b3e6f50366 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -10,6 +10,7 @@ using System.Linq; using osu.Framework.Extensions; using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Logging; +using osu.Game.Audio; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.Legacy; using osu.Game.Beatmaps.Timing; @@ -412,7 +413,7 @@ namespace osu.Game.Beatmaps.Formats string stringSampleSet = sampleSet.ToString().ToLowerInvariant(); if (stringSampleSet == @"none") - stringSampleSet = @"normal"; + stringSampleSet = HitSampleInfo.HIT_NORMAL; if (timingChange) { diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 03c63ff4f2..52d1ea60a5 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -547,13 +547,13 @@ namespace osu.Game.Beatmaps.Formats { switch (sampleBank?.ToLowerInvariant()) { - case "normal": + case HitSampleInfo.BANK_NORMAL: return LegacySampleBank.Normal; - case "soft": + case HitSampleInfo.BANK_SOFT: return LegacySampleBank.Soft; - case "drum": + case HitSampleInfo.BANK_DRUM: return LegacySampleBank.Drum; default: From 9222cb379f7c5cdb1ad7a25f73df28242e5b1406 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 19 Oct 2022 20:53:18 +0900 Subject: [PATCH 02/16] Add sample bank suport to editor selection handler --- .../Components/EditorSelectionHandler.cs | 73 ++++++++++++++++++- 1 file changed, 72 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs index 0bdfc5b0a0..1670328e58 100644 --- a/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs @@ -48,11 +48,40 @@ namespace osu.Game.Screens.Edit.Compose.Components /// public readonly Dictionary> SelectionSampleStates = new Dictionary>(); + /// + /// The state of each sample bank type for all selected hitobjects. + /// + public readonly Dictionary> SelectionBankStates = new Dictionary>(); + /// /// Set up ternary state bindables and bind them to selection/hitobject changes (in both directions) /// private void createStateBindables() { + foreach (string bankName in HitSampleInfo.AllBanks) + { + var bindable = new Bindable + { + Description = bankName.Titleize() + }; + + bindable.ValueChanged += state => + { + switch (state.NewValue) + { + case TernaryState.False: + RemoveSampleBank(bankName); + break; + + case TernaryState.True: + AddSampleBank(bankName); + break; + } + }; + + SelectionBankStates[bankName] = bindable; + } + foreach (string sampleName in HitSampleInfo.AllAdditions) { var bindable = new Bindable @@ -104,12 +133,48 @@ namespace osu.Game.Screens.Edit.Compose.Components { bindable.Value = GetStateFromSelection(SelectedItems, h => h.Samples.Any(s => s.Name == sampleName)); } + + foreach ((string bankName, var bindable) in SelectionBankStates) + { + bindable.Value = GetStateFromSelection(SelectedItems, h => h.SampleControlPoint.SampleBank == bankName); + } } #endregion #region Ternary state changes + /// + /// Adds a sample bank to all selected s. + /// + /// The name of the sample bank. + public void AddSampleBank(string bankName) + { + EditorBeatmap.PerformOnSelection(h => + { + if (h.SampleControlPoint.SampleBank == bankName) + return; + + h.SampleControlPoint.SampleBank = bankName; + EditorBeatmap.Update(h); + }); + } + + /// + /// Removes a sample bank from all selected s. + /// + /// The name of the sample bank. + public void RemoveSampleBank(string bankName) + { + EditorBeatmap.PerformOnSelection(h => + { + if (h.SampleControlPoint.SampleBank == bankName) + h.SampleControlPoint.SampleBankBindable.SetDefault(); + + EditorBeatmap.Update(h); + }); + } + /// /// Adds a hit sample to all selected s. /// @@ -174,11 +239,17 @@ namespace osu.Game.Screens.Edit.Compose.Components yield return new TernaryStateToggleMenuItem("New combo") { State = { BindTarget = SelectionNewComboState } }; } - yield return new OsuMenuItem("Sound") + yield return new OsuMenuItem("Sample") { Items = SelectionSampleStates.Select(kvp => new TernaryStateToggleMenuItem(kvp.Value.Description) { State = { BindTarget = kvp.Value } }).ToArray() }; + + yield return new OsuMenuItem("Bank") + { + Items = SelectionBankStates.Select(kvp => + new TernaryStateToggleMenuItem(kvp.Value.Description) { State = { BindTarget = kvp.Value } }).ToArray() + }; } #endregion From 50e24ddd872bcd8e6c5c543d8fa6fcecac0f7b3b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 19 Oct 2022 21:35:08 +0900 Subject: [PATCH 03/16] Add icon and radio button logic --- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 20 ++++++++- .../Components/ComposeBlueprintContainer.cs | 42 ++++++++++++++++++- .../Components/EditorSelectionHandler.cs | 35 +++++++++++++++- 3 files changed, 91 insertions(+), 6 deletions(-) diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 3bed835854..8857bb2dae 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -71,6 +71,8 @@ namespace osu.Game.Rulesets.Edit private FillFlowContainer togglesCollection; + private FillFlowContainer sampleBankTogglesCollection; + private IBindable hasTiming; protected HitObjectComposer(Ruleset ruleset) @@ -146,6 +148,16 @@ namespace osu.Game.Rulesets.Edit Direction = FillDirection.Vertical, Spacing = new Vector2(0, 5), }, + }, + new EditorToolboxGroup("bank (Shift-Q~R)") + { + Child = sampleBankTogglesCollection = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 5), + }, } } }, @@ -161,6 +173,8 @@ namespace osu.Game.Rulesets.Edit TernaryStates = CreateTernaryButtons().ToArray(); togglesCollection.AddRange(TernaryStates.Select(b => new DrawableTernaryButton(b))); + sampleBankTogglesCollection.AddRange(BlueprintContainer.SampleBankTernaryStates.Select(b => new DrawableTernaryButton(b))); + setSelectTool(); EditorBeatmap.SelectedHitObjects.CollectionChanged += selectionChanged; @@ -213,7 +227,7 @@ namespace osu.Game.Rulesets.Edit /// /// Create all ternary states required to be displayed to the user. /// - protected virtual IEnumerable CreateTernaryButtons() => BlueprintContainer.TernaryStates; + protected virtual IEnumerable CreateTernaryButtons() => BlueprintContainer.MainTernaryStates; /// /// Construct a relevant blueprint container. This will manage hitobject selection/placement input handling and display logic. @@ -255,7 +269,9 @@ namespace osu.Game.Rulesets.Edit if (checkRightToggleFromKey(e.Key, out int rightIndex)) { - var item = togglesCollection.ElementAtOrDefault(rightIndex); + var item = e.ShiftPressed + ? sampleBankTogglesCollection.ElementAtOrDefault(rightIndex) + : togglesCollection.ElementAtOrDefault(rightIndex); if (item is DrawableTernaryButton button) { diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index ec07da43a0..5adc60f6a7 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -14,6 +14,8 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; using osu.Game.Audio; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Tools; @@ -55,7 +57,8 @@ namespace osu.Game.Screens.Edit.Compose.Components [BackgroundDependencyLoader] private void load() { - TernaryStates = CreateTernaryButtons().ToArray(); + MainTernaryStates = CreateTernaryButtons().ToArray(); + SampleBankTernaryStates = createSampleBankTernaryButtons().ToArray(); AddInternal(placementBlueprintContainer); } @@ -172,7 +175,9 @@ namespace osu.Game.Screens.Edit.Compose.Components /// /// A collection of states which will be displayed to the user in the toolbox. /// - public TernaryButton[] TernaryStates { get; private set; } + public TernaryButton[] MainTernaryStates { get; private set; } + + public TernaryButton[] SampleBankTernaryStates { get; private set; } /// /// Create all ternary states required to be displayed to the user. @@ -186,6 +191,39 @@ namespace osu.Game.Screens.Edit.Compose.Components yield return new TernaryButton(kvp.Value, kvp.Key.Replace("hit", string.Empty).Titleize(), () => getIconForSample(kvp.Key)); } + private IEnumerable createSampleBankTernaryButtons() + { + foreach (var kvp in SelectionHandler.SelectionBankStates) + yield return new TernaryButton(kvp.Value, kvp.Key.Titleize(), () => getIconForBank(kvp.Key)); + } + + private Drawable getIconForBank(string sampleName) + { + return new Container + { + Size = new Vector2(30, 20), + Children = new Drawable[] + { + new SpriteIcon + { + Size = new Vector2(8), + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Icon = FontAwesome.Solid.VolumeOff + }, + new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + X = 10, + Y = -1, + Font = OsuFont.Default.With(weight: FontWeight.Bold, size: 20), + Text = $"{char.ToUpper(sampleName.First())}" + } + } + }; + } + private Drawable getIconForSample(string sampleName) { switch (sampleName) diff --git a/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs index 1670328e58..750dedac20 100644 --- a/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs @@ -11,6 +11,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics.UserInterface; using osu.Game.Audio; +using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; @@ -70,11 +71,38 @@ namespace osu.Game.Screens.Edit.Compose.Components switch (state.NewValue) { case TernaryState.False: - RemoveSampleBank(bankName); + if (SelectedItems.Count == 0) + { + // Ensure that if this is the last selected bank, it should remain selected. + if (SelectionBankStates.Values.All(b => b.Value == TernaryState.False)) + bindable.Value = TernaryState.True; + } + else + { + // Never remove a sample bank. + // These are basically radio buttons, not toggles. + if (SelectedItems.All(h => h.SampleControlPoint.SampleBank == bankName)) + bindable.Value = TernaryState.True; + } + break; case TernaryState.True: - AddSampleBank(bankName); + if (SelectedItems.Count == 0) + { + // Ensure the user can't stack multiple bank selections when there's no hitobject selection. + // Note that in normal scenarios this is sorted out by the feedback from applying the bank to the selected objects. + foreach (var other in SelectionBankStates.Values) + { + if (other != bindable) + other.Value = TernaryState.False; + } + } + else + { + AddSampleBank(bankName); + } + break; } }; @@ -82,6 +110,9 @@ namespace osu.Game.Screens.Edit.Compose.Components SelectionBankStates[bankName] = bindable; } + // start with normal selected. + SelectionBankStates[SampleControlPoint.DEFAULT_BANK].Value = TernaryState.True; + foreach (string sampleName in HitSampleInfo.AllAdditions) { var bindable = new Bindable From 372a655be1637d8823d89532d24a28f283798906 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 19 Oct 2022 21:39:51 +0900 Subject: [PATCH 04/16] Ensure placement uses currently selected bank --- .../Components/ComposeBlueprintContainer.cs | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index 5adc60f6a7..1c9ac83630 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -77,9 +77,10 @@ namespace osu.Game.Screens.Edit.Compose.Components // we own SelectionHandler so don't need to worry about making bindable copies (for simplicity) foreach (var kvp in SelectionHandler.SelectionSampleStates) - { kvp.Value.BindValueChanged(_ => updatePlacementSamples()); - } + + foreach (var kvp in SelectionHandler.SelectionBankStates) + kvp.Value.BindValueChanged(_ => updatePlacementSamples()); } protected override void TransferBlueprintFor(HitObject hitObject, DrawableHitObject drawableObject) @@ -146,6 +147,9 @@ namespace osu.Game.Screens.Edit.Compose.Components foreach (var kvp in SelectionHandler.SelectionSampleStates) sampleChanged(kvp.Key, kvp.Value.Value); + + foreach (var kvp in SelectionHandler.SelectionBankStates) + bankChanged(kvp.Key, kvp.Value.Value); } private void sampleChanged(string sampleName, TernaryState state) @@ -170,6 +174,18 @@ namespace osu.Game.Screens.Edit.Compose.Components } } + private void bankChanged(string bankName, TernaryState state) + { + if (currentPlacement == null) return; + + switch (state) + { + case TernaryState.True: + currentPlacement.HitObject.SampleControlPoint.SampleBank = bankName; + break; + } + } + public readonly Bindable NewCombo = new Bindable { Description = "New Combo" }; /// From b9f41611a7777951b8eebbb83ab5c9a6b27e1e12 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 19 Oct 2022 21:48:18 +0900 Subject: [PATCH 05/16] Fix bank potentially being overwritten during placement --- osu.Game/Rulesets/Edit/PlacementBlueprint.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs index c8196b6865..132214a0bd 100644 --- a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs @@ -74,9 +74,13 @@ namespace osu.Game.Rulesets.Edit /// Whether this call is committing a value for HitObject.StartTime and continuing with further adjustments. protected void BeginPlacement(bool commitStart = false) { + // Store and copy the bank, since it is managed by the editor UI. + string bank = HitObject.SampleControlPoint.SampleBank; + var nearestSampleControlPoint = beatmap.HitObjects.LastOrDefault(h => h.GetEndTime() < HitObject.StartTime)?.SampleControlPoint?.DeepClone() as SampleControlPoint; HitObject.SampleControlPoint = nearestSampleControlPoint ?? new SampleControlPoint(); + HitObject.SampleControlPoint.SampleBank = bank; placementHandler.BeginPlacement(HitObject); if (commitStart) From 677b8d09f8773626b36f0e686520fb871b53220d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 19 Oct 2022 23:54:12 +0900 Subject: [PATCH 06/16] Fix huge oversight causing test failures --- osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index b3e6f50366..ae57ee6f5a 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -413,7 +413,7 @@ namespace osu.Game.Beatmaps.Formats string stringSampleSet = sampleSet.ToString().ToLowerInvariant(); if (stringSampleSet == @"none") - stringSampleSet = HitSampleInfo.HIT_NORMAL; + stringSampleSet = HitSampleInfo.BANK_NORMAL; if (timingChange) { From 849b50a38fe821720944fff0b560f33dceb25286 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 21 Oct 2022 00:16:33 +0900 Subject: [PATCH 07/16] Use `ToUpperInvariant` for added safety MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- .../Edit/Compose/Components/ComposeBlueprintContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index 1c9ac83630..eebc4c8e0e 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -234,7 +234,7 @@ namespace osu.Game.Screens.Edit.Compose.Components X = 10, Y = -1, Font = OsuFont.Default.With(weight: FontWeight.Bold, size: 20), - Text = $"{char.ToUpper(sampleName.First())}" + Text = $"{char.ToUpperInvariant(sampleName.First())}" } } }; From e68ba6366c88554db3bee4332be5f1563e4257c1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 24 May 2023 14:04:10 +0900 Subject: [PATCH 08/16] Update new usages of "soft" to use the new constant --- .../Editor/TestSceneSliderSplitting.cs | 2 +- .../TestSceneDrumSampleTriggerSource.cs | 24 +++++++++---------- .../TestSceneHitObjectSampleAdjustments.cs | 10 ++++---- .../TestSceneGameplaySampleTriggerSource.cs | 2 +- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs index a104433ea9..605771fb20 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs @@ -181,7 +181,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { if (slider is null) return; - sample = new HitSampleInfo("hitwhistle", "soft", volume: 70); + sample = new HitSampleInfo("hitwhistle", HitSampleInfo.BANK_SOFT, volume: 70); slider.Samples.Add(sample.With()); }); diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumSampleTriggerSource.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumSampleTriggerSource.cs index 74da69e3eb..287d90b406 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumSampleTriggerSource.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumSampleTriggerSource.cs @@ -91,7 +91,7 @@ namespace osu.Game.Rulesets.Taiko.Tests StartTime = 100, Samples = new List { - new HitSampleInfo(HitSampleInfo.HIT_NORMAL, "soft") + new HitSampleInfo(HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT) } }; hit.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); @@ -100,13 +100,13 @@ namespace osu.Game.Rulesets.Taiko.Tests }); AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "soft"); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "soft"); + checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); + checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); AddStep("seek past hit", () => manualClock.CurrentTime = 200); AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "soft"); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "soft"); + checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); + checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); } [Test] @@ -183,7 +183,7 @@ namespace osu.Game.Rulesets.Taiko.Tests EndTime = 1100, Samples = new List { - new HitSampleInfo(HitSampleInfo.HIT_NORMAL, "soft") + new HitSampleInfo(HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT) } }; drumRoll.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); @@ -192,18 +192,18 @@ namespace osu.Game.Rulesets.Taiko.Tests }); AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "soft"); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "soft"); + checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); + checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); AddStep("seek to middle of drum roll", () => manualClock.CurrentTime = 600); AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "soft"); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "soft"); + checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); + checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); AddStep("seek past drum roll", () => manualClock.CurrentTime = 1200); AddAssert("most valid object is drum roll", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "soft"); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "soft"); + checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); + checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); } [Test] diff --git a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs index 0581ff269c..eb39221211 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs @@ -52,7 +52,7 @@ namespace osu.Game.Tests.Visual.Editing Position = (OsuPlayfield.BASE_SIZE + new Vector2(100, 0)) / 2, Samples = new List { - new HitSampleInfo(HitSampleInfo.HIT_NORMAL, "soft", volume: 60) + new HitSampleInfo(HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT, volume: 60) } }); }); @@ -67,14 +67,14 @@ namespace osu.Game.Tests.Visual.Editing hitObjectHasSampleBank(0, "normal"); hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_CLAP); - hitObjectHasSampleBank(1, "soft"); + hitObjectHasSampleBank(1, HitSampleInfo.BANK_SOFT); hitObjectHasSamples(1, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_CLAP); AddStep("remove clap addition", () => InputManager.Key(Key.R)); hitObjectHasSampleBank(0, "normal"); hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL); - hitObjectHasSampleBank(1, "soft"); + hitObjectHasSampleBank(1, HitSampleInfo.BANK_SOFT); hitObjectHasSamples(1, HitSampleInfo.HIT_NORMAL); } @@ -113,7 +113,7 @@ namespace osu.Game.Tests.Visual.Editing public void TestUndo() { clickSamplePiece(1); - samplePopoverHasSingleBank("soft"); + samplePopoverHasSingleBank(HitSampleInfo.BANK_SOFT); samplePopoverHasSingleVolume(60); setVolumeViaPopover(90); @@ -178,7 +178,7 @@ namespace osu.Game.Tests.Visual.Editing { for (int i = 0; i < h.Samples.Count; i++) { - h.Samples[i] = h.Samples[i].With(newBank: "soft"); + h.Samples[i] = h.Samples[i].With(newBank: HitSampleInfo.BANK_SOFT); } } }); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs index bf69da8c12..e52ec6f8cc 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs @@ -83,7 +83,7 @@ namespace osu.Game.Tests.Visual.Gameplay { StartTime = t += spacing, Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, Vector2.UnitY * 200 }), - Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_WHISTLE, "soft") }, + Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_WHISTLE, HitSampleInfo.BANK_SOFT) }, }, }); From a9ba16a2be5d8355248d658ddb90867799dc86ba Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 24 May 2023 14:20:38 +0900 Subject: [PATCH 09/16] Update to support non-control-point sample changes --- .../Components/ComposeBlueprintContainer.cs | 4 ++-- .../Components/EditorSelectionHandler.cs | 23 ++++--------------- 2 files changed, 6 insertions(+), 21 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index b4f2236847..3f3b4ad327 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -199,12 +199,12 @@ namespace osu.Game.Screens.Edit.Compose.Components private void bankChanged(string bankName, TernaryState state) { - if (currentPlacement == null) return; + if (CurrentPlacement == null) return; switch (state) { case TernaryState.True: - currentPlacement.HitObject.SampleControlPoint.SampleBank = bankName; + CurrentPlacement.HitObject.Samples = CurrentPlacement.HitObject.Samples.Select(s => s.With(newBank: bankName)).ToList(); break; } } diff --git a/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs index 5d2eb33d8d..fa3a4cddaa 100644 --- a/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs @@ -81,7 +81,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { // Never remove a sample bank. // These are basically radio buttons, not toggles. - if (SelectedItems.All(h => h.SampleControlPoint.SampleBank == bankName)) + if (SelectedItems.All(h => h.Samples.All(s => s.Bank == bankName))) bindable.Value = TernaryState.True; } @@ -167,7 +167,7 @@ namespace osu.Game.Screens.Edit.Compose.Components foreach ((string bankName, var bindable) in SelectionBankStates) { - bindable.Value = GetStateFromSelection(SelectedItems, h => h.SampleControlPoint.SampleBank == bankName); + bindable.Value = GetStateFromSelection(SelectedItems, h => h.Samples.All(s => s.Bank == bankName)); } } @@ -183,25 +183,10 @@ namespace osu.Game.Screens.Edit.Compose.Components { EditorBeatmap.PerformOnSelection(h => { - if (h.SampleControlPoint.SampleBank == bankName) + if (h.Samples.All(s => s.Bank == bankName)) return; - h.SampleControlPoint.SampleBank = bankName; - EditorBeatmap.Update(h); - }); - } - - /// - /// Removes a sample bank from all selected s. - /// - /// The name of the sample bank. - public void RemoveSampleBank(string bankName) - { - EditorBeatmap.PerformOnSelection(h => - { - if (h.SampleControlPoint.SampleBank == bankName) - h.SampleControlPoint.SampleBankBindable.SetDefault(); - + h.Samples = h.Samples.Select(s => s.With(newBank: bankName)).ToList(); EditorBeatmap.Update(h); }); } From a22ad98cb79567018833c14e9cc845e02bf564cb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 24 May 2023 14:56:11 +0900 Subject: [PATCH 10/16] Fix hotkeys not actually working --- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 62c3211e85..106ffc8ee7 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -288,7 +288,7 @@ namespace osu.Game.Rulesets.Edit protected override bool OnKeyDown(KeyDownEvent e) { - if (e.ControlPressed || e.AltPressed || e.SuperPressed || e.ShiftPressed) + if (e.ControlPressed || e.AltPressed || e.SuperPressed) return false; if (checkLeftToggleFromKey(e.Key, out int leftIndex)) From fc22c754641b2b11c4f4ecaf714df70cf1fd6b0c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 24 May 2023 16:14:05 +0900 Subject: [PATCH 11/16] Don't use `switch` for single `case` statement --- .../Edit/Compose/Components/ComposeBlueprintContainer.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index 3f3b4ad327..d2dd83eb1a 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -201,12 +201,8 @@ namespace osu.Game.Screens.Edit.Compose.Components { if (CurrentPlacement == null) return; - switch (state) - { - case TernaryState.True: - CurrentPlacement.HitObject.Samples = CurrentPlacement.HitObject.Samples.Select(s => s.With(newBank: bankName)).ToList(); - break; - } + if (state == TernaryState.True) + CurrentPlacement.HitObject.Samples = CurrentPlacement.HitObject.Samples.Select(s => s.With(newBank: bankName)).ToList(); } public readonly Bindable NewCombo = new Bindable { Description = "New Combo" }; From 3a05dffa506aa0babb6dec23356a8cc21c763d10 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 24 May 2023 17:11:12 +0900 Subject: [PATCH 12/16] Add "auto" bank selection during placement --- osu.Game/Rulesets/Edit/PlacementBlueprint.cs | 18 +++++++++++++----- .../Components/ComposeBlueprintContainer.cs | 2 ++ .../Components/EditorSelectionHandler.cs | 16 +++++++++++++++- 3 files changed, 30 insertions(+), 6 deletions(-) diff --git a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs index 551e557599..a0a04c13d0 100644 --- a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs @@ -32,6 +32,11 @@ namespace osu.Game.Rulesets.Edit /// public PlacementState PlacementActive { get; private set; } + /// + /// Whether the sample bank should be taken from the previous hit object. + /// + public bool AutomaticBankAssignment; + /// /// The that is being placed. /// @@ -86,11 +91,6 @@ namespace osu.Game.Rulesets.Edit /// Whether this call is committing a value for HitObject.StartTime and continuing with further adjustments. protected void BeginPlacement(bool commitStart = false) { - // Take the hitnormal sample of the last hit object - var lastHitNormal = getPreviousHitObject()?.Samples?.FirstOrDefault(o => o.Name == HitSampleInfo.HIT_NORMAL); - if (lastHitNormal != null) - HitObject.Samples[0] = lastHitNormal; - placementHandler.BeginPlacement(HitObject); if (commitStart) PlacementActive = PlacementState.Active; @@ -155,6 +155,14 @@ namespace osu.Game.Rulesets.Edit if (HitObject is IHasComboInformation comboInformation) comboInformation.UpdateComboInformation(getPreviousHitObject() as IHasComboInformation); } + + if (AutomaticBankAssignment) + { + // Take the hitnormal sample of the last hit object + var lastHitNormal = getPreviousHitObject()?.Samples?.FirstOrDefault(o => o.Name == HitSampleInfo.HIT_NORMAL); + if (lastHitNormal != null) + HitObject.Samples[0] = lastHitNormal; + } } /// diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index d2dd83eb1a..25babae6ff 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -201,6 +201,8 @@ namespace osu.Game.Screens.Edit.Compose.Components { if (CurrentPlacement == null) return; + if (bankName == EditorSelectionHandler.HIT_BANK_AUTO) + CurrentPlacement.AutomaticBankAssignment = state == TernaryState.True; if (state == TernaryState.True) CurrentPlacement.HitObject.Samples = CurrentPlacement.HitObject.Samples.Select(s => s.With(newBank: bankName)).ToList(); } diff --git a/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs index fa3a4cddaa..dc7e12ea93 100644 --- a/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs @@ -21,6 +21,12 @@ namespace osu.Game.Screens.Edit.Compose.Components { public partial class EditorSelectionHandler : SelectionHandler { + /// + /// A special bank name that is only used in the editor UI. + /// When selected and in placement mode, the bank of the last hit object will always be used. + /// + public const string HIT_BANK_AUTO = "auto"; + [Resolved] protected EditorBeatmap EditorBeatmap { get; private set; } @@ -59,7 +65,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// private void createStateBindables() { - foreach (string bankName in HitSampleInfo.AllBanks) + foreach (string bankName in HitSampleInfo.AllBanks.Prepend(HIT_BANK_AUTO)) { var bindable = new Bindable { @@ -100,6 +106,14 @@ namespace osu.Game.Screens.Edit.Compose.Components } else { + // Auto should just not apply if there's a selection already made. + // Maybe we could make it a disabled button in the future, but right now the editor buttons don't support disabled state. + if (bankName == HIT_BANK_AUTO) + { + bindable.Value = TernaryState.False; + break; + } + AddSampleBank(bankName); } From 8e5ba2208d29451882378130e2be0854a7b142a6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 24 May 2023 17:33:48 +0900 Subject: [PATCH 13/16] Add test coverage of new hotkeys --- .../TestSceneHitObjectSampleAdjustments.cs | 100 +++++++++++++++++- 1 file changed, 98 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs index eb39221211..b0b51a5dbd 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs @@ -13,6 +13,7 @@ using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Rulesets; +using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.UI; @@ -170,7 +171,7 @@ namespace osu.Game.Tests.Visual.Editing } [Test] - public void TestMultipleSelectionWithSameSampleBank() + public void TestPopoverMultipleSelectionWithSameSampleBank() { AddStep("unify sample bank", () => { @@ -204,7 +205,7 @@ namespace osu.Game.Tests.Visual.Editing } [Test] - public void TestMultipleSelectionWithDifferentSampleBank() + public void TestPopoverMultipleSelectionWithDifferentSampleBank() { AddStep("select both objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects)); clickSamplePiece(0); @@ -226,6 +227,101 @@ namespace osu.Game.Tests.Visual.Editing samplePopoverHasSingleBank(HitSampleInfo.BANK_NORMAL); } + [Test] + public void TestHotkeysMultipleSelectionWithSameSampleBank() + { + AddStep("unify sample bank", () => + { + foreach (var h in EditorBeatmap.HitObjects) + { + for (int i = 0; i < h.Samples.Count; i++) + { + h.Samples[i] = h.Samples[i].With(newBank: HitSampleInfo.BANK_SOFT); + } + } + }); + + AddStep("select both objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects)); + + hitObjectHasSampleBank(0, HitSampleInfo.BANK_SOFT); + hitObjectHasSampleBank(1, HitSampleInfo.BANK_SOFT); + + AddStep("Press normal bank shortcut", () => + { + InputManager.PressKey(Key.ShiftLeft); + InputManager.Key(Key.W); + InputManager.ReleaseKey(Key.ShiftLeft); + }); + + hitObjectHasSampleBank(0, HitSampleInfo.BANK_NORMAL); + hitObjectHasSampleBank(1, HitSampleInfo.BANK_NORMAL); + + AddStep("Press drum bank shortcut", () => + { + InputManager.PressKey(Key.ShiftLeft); + InputManager.Key(Key.R); + InputManager.ReleaseKey(Key.ShiftLeft); + }); + + hitObjectHasSampleBank(0, HitSampleInfo.BANK_DRUM); + hitObjectHasSampleBank(1, HitSampleInfo.BANK_DRUM); + + AddStep("Press auto bank shortcut", () => + { + InputManager.PressKey(Key.ShiftLeft); + InputManager.Key(Key.Q); + InputManager.ReleaseKey(Key.ShiftLeft); + }); + + // Should be a noop. + hitObjectHasSampleBank(0, HitSampleInfo.BANK_DRUM); + hitObjectHasSampleBank(1, HitSampleInfo.BANK_DRUM); + } + + [Test] + public void TestHotkeysDuringPlacement() + { + AddStep("Enter placement mode", () => InputManager.Key(Key.Number2)); + AddStep("Move mouse to centre", () => InputManager.MoveMouseTo(Editor.ChildrenOfType().First().ScreenSpaceDrawQuad.Centre)); + + AddStep("Move between two objects", () => EditorClock.Seek(250)); + + AddStep("Press normal bank shortcut", () => + { + InputManager.PressKey(Key.ShiftLeft); + InputManager.Key(Key.W); + InputManager.ReleaseKey(Key.ShiftLeft); + }); + + checkPlacementSample(HitSampleInfo.BANK_NORMAL); + + AddStep("Press drum bank shortcut", () => + { + InputManager.PressKey(Key.ShiftLeft); + InputManager.Key(Key.R); + InputManager.ReleaseKey(Key.ShiftLeft); + }); + + checkPlacementSample(HitSampleInfo.BANK_DRUM); + + AddStep("Press auto bank shortcut", () => + { + InputManager.PressKey(Key.ShiftLeft); + InputManager.Key(Key.Q); + InputManager.ReleaseKey(Key.ShiftLeft); + }); + + checkPlacementSample(HitSampleInfo.BANK_NORMAL); + + AddStep("Move after second object", () => EditorClock.Seek(750)); + checkPlacementSample(HitSampleInfo.BANK_SOFT); + + AddStep("Move to first object", () => EditorClock.Seek(0)); + checkPlacementSample(HitSampleInfo.BANK_NORMAL); + + void checkPlacementSample(string expected) => AddAssert($"Placement sample is {expected}", () => EditorBeatmap.PlacementObject.Value.Samples.First().Bank, () => Is.EqualTo(expected)); + } + private void clickSamplePiece(int objectIndex) => AddStep($"click {objectIndex.ToOrdinalWords()} sample piece", () => { var samplePiece = this.ChildrenOfType().Single(piece => piece.HitObject == EditorBeatmap.HitObjects.ElementAt(objectIndex)); From 1e14b024938a584dc61725e4ed5d423459a8f29c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 25 May 2023 17:57:37 +0900 Subject: [PATCH 14/16] Fix bindable feedback loop --- .../Edit/Compose/Components/EditorSelectionHandler.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs index dc7e12ea93..d618541685 100644 --- a/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs @@ -85,6 +85,11 @@ namespace osu.Game.Screens.Edit.Compose.Components } else { + // Auto should never apply when there is a selection made. + // This is also required to stop a bindable feedback loop when a HitObject has zero samples (and LINQ `All` below becomes true). + if (bankName == HIT_BANK_AUTO) + break; + // Never remove a sample bank. // These are basically radio buttons, not toggles. if (SelectedItems.All(h => h.Samples.All(s => s.Bank == bankName))) From adee624a8fe86da21a3f1377aae10aa96eec9007 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 25 May 2023 21:32:19 +0200 Subject: [PATCH 15/16] Change `PlacementBlueprint.AutomaticBankAssignment` to property Mostly for consistency. --- osu.Game/Rulesets/Edit/PlacementBlueprint.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs index a0a04c13d0..717c026ded 100644 --- a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs @@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Edit /// /// Whether the sample bank should be taken from the previous hit object. /// - public bool AutomaticBankAssignment; + public bool AutomaticBankAssignment { get; set; } /// /// The that is being placed. From 7d8f08c0ea0e2e1f6d2952926f85557ff9447350 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 25 May 2023 21:49:29 +0200 Subject: [PATCH 16/16] Fix `ComposeBlueprintContainer` briefly assigning `auto` bank Seems to have had no consequence due to the way `AutomaticBankAssignment` works (that flag is checked in `PlacementBlueprint.UpdateTimeAndPosition()`, which runs essentially every frame), but let's avoid putting it there at all ever. --- .../Edit/Compose/Components/ComposeBlueprintContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index 25babae6ff..c8cfac454a 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -203,7 +203,7 @@ namespace osu.Game.Screens.Edit.Compose.Components if (bankName == EditorSelectionHandler.HIT_BANK_AUTO) CurrentPlacement.AutomaticBankAssignment = state == TernaryState.True; - if (state == TernaryState.True) + else if (state == TernaryState.True) CurrentPlacement.HitObject.Samples = CurrentPlacement.HitObject.Samples.Select(s => s.With(newBank: bankName)).ToList(); }