From 0a78eb96289c5f38ab8447daeaedbcb55a290a55 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Thu, 29 Aug 2024 21:54:47 +0200 Subject: [PATCH] Implement auto additions editor-only --- osu.Game/Audio/HitSampleInfo.cs | 13 +++- .../Beatmaps/Formats/LegacyBeatmapEncoder.cs | 2 +- osu.Game/Rulesets/Objects/HitObject.cs | 2 +- .../Objects/Legacy/ConvertHitObjectParser.cs | 33 ++++++---- .../Components/EditorSelectionHandler.cs | 60 ++++++++----------- .../Components/Timeline/SamplePointPiece.cs | 37 ++++++++---- 6 files changed, 87 insertions(+), 60 deletions(-) diff --git a/osu.Game/Audio/HitSampleInfo.cs b/osu.Game/Audio/HitSampleInfo.cs index ce5e217532..19273e3714 100644 --- a/osu.Game/Audio/HitSampleInfo.cs +++ b/osu.Game/Audio/HitSampleInfo.cs @@ -60,12 +60,18 @@ namespace osu.Game.Audio /// public int Volume { get; } - public HitSampleInfo(string name, string bank = SampleControlPoint.DEFAULT_BANK, string? suffix = null, int volume = 100) + /// + /// Whether this sample should automatically assign the bank of the normal sample whenever it is set in the editor. + /// + public bool EditorAutoBank { get; } + + public HitSampleInfo(string name, string bank = SampleControlPoint.DEFAULT_BANK, string? suffix = null, int volume = 100, bool editorAutoBank = true) { Name = name; Bank = bank; Suffix = suffix; Volume = volume; + EditorAutoBank = editorAutoBank; } /// @@ -92,9 +98,10 @@ namespace osu.Game.Audio /// An optional new sample bank. /// An optional new lookup suffix. /// An optional new volume. + /// An optional new editor auto bank flag. /// The new . - public virtual HitSampleInfo With(Optional newName = default, Optional newBank = default, Optional newSuffix = default, Optional newVolume = default) - => new HitSampleInfo(newName.GetOr(Name), newBank.GetOr(Bank), newSuffix.GetOr(Suffix), newVolume.GetOr(Volume)); + public virtual HitSampleInfo With(Optional newName = default, Optional newBank = default, Optional newSuffix = default, Optional newVolume = default, Optional newEditorAutoBank = default) + => new HitSampleInfo(newName.GetOr(Name), newBank.GetOr(Bank), newSuffix.GetOr(Suffix), newVolume.GetOr(Volume), newEditorAutoBank.GetOr(EditorAutoBank)); public virtual bool Equals(HitSampleInfo? other) => other != null && Name == other.Name && Bank == other.Bank && Suffix == other.Suffix; diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index b0173b3ae3..c14648caf6 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -539,7 +539,7 @@ namespace osu.Game.Beatmaps.Formats private string getSampleBank(IList samples, bool banksOnly = false) { LegacySampleBank normalBank = toLegacySampleBank(samples.SingleOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL)?.Bank); - LegacySampleBank addBank = toLegacySampleBank(samples.FirstOrDefault(s => !string.IsNullOrEmpty(s.Name) && s.Name != HitSampleInfo.HIT_NORMAL)?.Bank); + LegacySampleBank addBank = toLegacySampleBank(samples.FirstOrDefault(s => !string.IsNullOrEmpty(s.Name) && s.Name != HitSampleInfo.HIT_NORMAL && !s.EditorAutoBank)?.Bank); StringBuilder sb = new StringBuilder(); diff --git a/osu.Game/Rulesets/Objects/HitObject.cs b/osu.Game/Rulesets/Objects/HitObject.cs index c547a7a718..9f980769e2 100644 --- a/osu.Game/Rulesets/Objects/HitObject.cs +++ b/osu.Game/Rulesets/Objects/HitObject.cs @@ -233,7 +233,7 @@ namespace osu.Game.Rulesets.Objects // Fall back to using the normal sample bank otherwise. if (Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL) is HitSampleInfo existingNormal) - return existingNormal.With(newName: sampleName); + return existingNormal.With(newName: sampleName, newEditorAutoBank: true); return new HitSampleInfo(sampleName); } diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index c518a3e8b2..fa3ee41b47 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -204,8 +204,14 @@ namespace osu.Game.Rulesets.Objects.Legacy if (stringBank == @"none") stringBank = null; string stringAddBank = addBank.ToString().ToLowerInvariant(); + if (stringAddBank == @"none") + { + bankInfo.EditorAutoBank = true; stringAddBank = null; + } + else + bankInfo.EditorAutoBank = false; bankInfo.BankForNormal = stringBank; bankInfo.BankForAdditions = string.IsNullOrEmpty(stringAddBank) ? stringBank : stringAddBank; @@ -477,7 +483,7 @@ namespace osu.Game.Rulesets.Objects.Legacy if (string.IsNullOrEmpty(bankInfo.Filename)) { - soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_NORMAL, bankInfo.BankForNormal, bankInfo.Volume, bankInfo.CustomSampleBank, + soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_NORMAL, bankInfo.BankForNormal, bankInfo.Volume, true, bankInfo.CustomSampleBank, // if the sound type doesn't have the Normal flag set, attach it anyway as a layered sample. // None also counts as a normal non-layered sample: https://osu.ppy.sh/help/wiki/osu!_File_Formats/Osu_(file_format)#hitsounds type != LegacyHitSoundType.None && !type.HasFlag(LegacyHitSoundType.Normal))); @@ -489,13 +495,13 @@ namespace osu.Game.Rulesets.Objects.Legacy } if (type.HasFlag(LegacyHitSoundType.Finish)) - soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_FINISH, bankInfo.BankForAdditions, bankInfo.Volume, bankInfo.CustomSampleBank)); + soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_FINISH, bankInfo.BankForAdditions, bankInfo.Volume, bankInfo.EditorAutoBank, bankInfo.CustomSampleBank)); if (type.HasFlag(LegacyHitSoundType.Whistle)) - soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_WHISTLE, bankInfo.BankForAdditions, bankInfo.Volume, bankInfo.CustomSampleBank)); + soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_WHISTLE, bankInfo.BankForAdditions, bankInfo.Volume, bankInfo.EditorAutoBank, bankInfo.CustomSampleBank)); if (type.HasFlag(LegacyHitSoundType.Clap)) - soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_CLAP, bankInfo.BankForAdditions, bankInfo.Volume, bankInfo.CustomSampleBank)); + soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_CLAP, bankInfo.BankForAdditions, bankInfo.Volume, bankInfo.EditorAutoBank, bankInfo.CustomSampleBank)); return soundTypes; } @@ -534,6 +540,11 @@ namespace osu.Game.Rulesets.Objects.Legacy /// public int CustomSampleBank; + /// + /// Whether the bank for additions should be inherited from the normal sample in edit. + /// + public bool EditorAutoBank; + public SampleBankInfo Clone() => (SampleBankInfo)MemberwiseClone(); } @@ -558,21 +569,21 @@ namespace osu.Game.Rulesets.Objects.Legacy /// public bool BankSpecified; - public LegacyHitSampleInfo(string name, string? bank = null, int volume = 0, int customSampleBank = 0, bool isLayered = false) - : base(name, bank ?? SampleControlPoint.DEFAULT_BANK, customSampleBank >= 2 ? customSampleBank.ToString() : null, volume) + public LegacyHitSampleInfo(string name, string? bank = null, int volume = 0, bool editorAutoBank = false, int customSampleBank = 0, bool isLayered = false) + : base(name, bank ?? SampleControlPoint.DEFAULT_BANK, customSampleBank >= 2 ? customSampleBank.ToString() : null, volume, editorAutoBank) { CustomSampleBank = customSampleBank; BankSpecified = !string.IsNullOrEmpty(bank); IsLayered = isLayered; } - public sealed override HitSampleInfo With(Optional newName = default, Optional newBank = default, Optional newSuffix = default, Optional newVolume = default) - => With(newName, newBank, newVolume); + 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, + 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), newCustomSampleBank.GetOr(CustomSampleBank), newIsLayered.GetOr(IsLayered)); + => 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. @@ -604,7 +615,7 @@ namespace osu.Game.Rulesets.Objects.Legacy Path.ChangeExtension(Filename, null) }; - public sealed override LegacyHitSampleInfo With(Optional newName = default, Optional newBank = default, Optional newVolume = 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)); diff --git a/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs index 769240839a..8de3f3052f 100644 --- a/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs @@ -152,10 +152,6 @@ namespace osu.Game.Screens.Edit.Compose.Components } else { - // Auto should never apply when there is a selection made. - if (bankName == HIT_BANK_AUTO) - break; - // Completely empty selections should be allowed in the case that none of the selected objects have any addition samples. // This is also required to stop a bindable feedback loop when a HitObject has zero addition samples (and LINQ `All` below becomes true). if (SelectedItems.SelectMany(enumerateAllSamples).All(h => h.All(o => o.Name == HitSampleInfo.HIT_NORMAL))) @@ -163,8 +159,16 @@ namespace osu.Game.Screens.Edit.Compose.Components // Never remove a sample bank. // These are basically radio buttons, not toggles. - if (SelectedItems.SelectMany(enumerateAllSamples).All(h => h.Where(o => o.Name != HitSampleInfo.HIT_NORMAL).All(s => s.Bank == bankName))) - bindable.Value = TernaryState.True; + if (bankName == HIT_BANK_AUTO) + { + if (SelectedItems.SelectMany(enumerateAllSamples).All(h => h.Where(o => o.Name != HitSampleInfo.HIT_NORMAL).All(s => s.EditorAutoBank))) + bindable.Value = TernaryState.True; + } + else + { + if (SelectedItems.SelectMany(enumerateAllSamples).All(h => h.Where(o => o.Name != HitSampleInfo.HIT_NORMAL).All(s => s.Bank == bankName && !s.EditorAutoBank))) + bindable.Value = TernaryState.True; + } } break; @@ -182,14 +186,6 @@ 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; - } - // If none of the selected objects have any addition samples, we should not apply the addition bank. if (SelectedItems.SelectMany(enumerateAllSamples).All(h => h.All(o => o.Name == HitSampleInfo.HIT_NORMAL))) { @@ -209,7 +205,7 @@ namespace osu.Game.Screens.Edit.Compose.Components // start with normal selected. SelectionBankStates[SampleControlPoint.DEFAULT_BANK].Value = TernaryState.True; - SelectionAdditionBankStates[SampleControlPoint.DEFAULT_BANK].Value = TernaryState.True; + SelectionAdditionBankStates[HIT_BANK_AUTO].Value = TernaryState.True; foreach (string sampleName in HitSampleInfo.AllAdditions) { @@ -272,7 +268,7 @@ namespace osu.Game.Screens.Edit.Compose.Components foreach ((string bankName, var bindable) in SelectionAdditionBankStates) { - bindable.Value = GetStateFromSelection(samplesInSelection.SelectMany(s => s).Where(o => o.Name != HitSampleInfo.HIT_NORMAL), h => h.Bank == bankName); + bindable.Value = GetStateFromSelection(samplesInSelection.SelectMany(s => s).Where(o => o.Name != HitSampleInfo.HIT_NORMAL), h => (bankName != HIT_BANK_AUTO && h.Bank == bankName && !h.EditorAutoBank) || (bankName == HIT_BANK_AUTO && h.EditorAutoBank)); } } @@ -336,33 +332,29 @@ namespace osu.Game.Screens.Edit.Compose.Components /// The name of the sample bank. public void SetSampleAdditionBank(string bankName) { - bool hasRelevantBank(HitObject hitObject) - { - bool result = hitObject.Samples.Where(o => o.Name != HitSampleInfo.HIT_NORMAL).All(s => s.Bank == bankName); - - if (hitObject is IHasRepeats hasRepeats) - { - foreach (var node in hasRepeats.NodeSamples) - result &= node.Where(o => o.Name != HitSampleInfo.HIT_NORMAL).All(s => s.Bank == bankName); - } - - return result; - } + bool hasRelevantBank(HitObject hitObject) => + bankName == HIT_BANK_AUTO + ? enumerateAllSamples(hitObject).SelectMany(o => o).Where(o => o.Name != HitSampleInfo.HIT_NORMAL).All(s => s.EditorAutoBank) + : enumerateAllSamples(hitObject).SelectMany(o => o).Where(o => o.Name != HitSampleInfo.HIT_NORMAL).All(s => s.Bank == bankName && !s.EditorAutoBank); if (SelectedItems.All(hasRelevantBank)) return; EditorBeatmap.PerformOnSelection(h => { - if (enumerateAllSamples(h).SelectMany(o => o).Where(o => o.Name != HitSampleInfo.HIT_NORMAL).All(s => s.Bank == bankName)) + if (hasRelevantBank(h)) return; - h.Samples = h.Samples.Select(s => s.Name != HitSampleInfo.HIT_NORMAL ? s.With(newBank: bankName) : s).ToList(); + string normalBank = h.Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL)?.Bank ?? HitSampleInfo.BANK_SOFT; + h.Samples = h.Samples.Select(s => s.Name != HitSampleInfo.HIT_NORMAL ? bankName == HIT_BANK_AUTO ? s.With(newBank: normalBank, newEditorAutoBank: true) : s.With(newBank: bankName, newEditorAutoBank: false) : s).ToList(); if (h is IHasRepeats hasRepeats) { for (int i = 0; i < hasRepeats.NodeSamples.Count; ++i) - hasRepeats.NodeSamples[i] = hasRepeats.NodeSamples[i].Select(s => s.Name != HitSampleInfo.HIT_NORMAL ? s.With(newBank: bankName) : s).ToList(); + { + normalBank = hasRepeats.NodeSamples[i].FirstOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL)?.Bank ?? HitSampleInfo.BANK_SOFT; + hasRepeats.NodeSamples[i] = hasRepeats.NodeSamples[i].Select(s => s.Name != HitSampleInfo.HIT_NORMAL ? bankName == HIT_BANK_AUTO ? s.With(newBank: normalBank, newEditorAutoBank: true) : s.With(newBank: bankName, newEditorAutoBank: false) : s).ToList(); + } } EditorBeatmap.Update(h); @@ -406,9 +398,9 @@ namespace osu.Game.Screens.Edit.Compose.Components var hitSample = h.CreateHitSampleInfo(sampleName); - string? existingAdditionBank = node.FirstOrDefault(s => s.Name != HitSampleInfo.HIT_NORMAL)?.Bank; - if (existingAdditionBank != null) - hitSample = hitSample.With(newBank: existingAdditionBank); + HitSampleInfo? existingAddition = node.FirstOrDefault(s => s.Name != HitSampleInfo.HIT_NORMAL); + if (existingAddition != null) + hitSample = hitSample.With(newBank: existingAddition.Bank, newEditorAutoBank: existingAddition.EditorAutoBank); node.Add(hitSample); } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index 6276090557..07833694c5 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -107,7 +107,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline public static string? GetAdditionBankValue(IEnumerable samples) { - return samples.FirstOrDefault(o => o.Name != HitSampleInfo.HIT_NORMAL)?.Bank; + var firstAddition = samples.FirstOrDefault(o => o.Name != HitSampleInfo.HIT_NORMAL); + if (firstAddition == null) + return null; + + return firstAddition.EditorAutoBank ? EditorSelectionHandler.HIT_BANK_AUTO : firstAddition.Bank; } public static int GetVolumeValue(ICollection samples) @@ -320,7 +324,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { for (int i = 0; i < relevantSamples.Count; i++) { - if (relevantSamples[i].Name != HitSampleInfo.HIT_NORMAL) continue; + if (relevantSamples[i].Name != HitSampleInfo.HIT_NORMAL && !relevantSamples[i].EditorAutoBank) continue; relevantSamples[i] = relevantSamples[i].With(newBank: newBank); } @@ -331,11 +335,20 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { updateAllRelevantSamples((_, relevantSamples) => { + string normalBank = relevantSamples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL)?.Bank ?? HitSampleInfo.BANK_SOFT; + for (int i = 0; i < relevantSamples.Count; i++) { - if (relevantSamples[i].Name == HitSampleInfo.HIT_NORMAL) continue; + if (relevantSamples[i].Name == HitSampleInfo.HIT_NORMAL) + continue; - relevantSamples[i] = relevantSamples[i].With(newBank: newBank); + // Addition samples with bank set to auto should inherit the bank of the normal sample + if (newBank == EditorSelectionHandler.HIT_BANK_AUTO) + { + relevantSamples[i] = relevantSamples[i].With(newBank: normalBank, newEditorAutoBank: true); + } + else + relevantSamples[i] = relevantSamples[i].With(newBank: newBank, newEditorAutoBank: false); } }); } @@ -383,7 +396,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline selectionSampleStates[sampleName] = bindable; } - banks.AddRange(HitSampleInfo.AllBanks); + banks.AddRange(HitSampleInfo.AllBanks.Prepend(EditorSelectionHandler.HIT_BANK_AUTO)); } private void updateTernaryStates() @@ -448,7 +461,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline if (string.IsNullOrEmpty(newBank)) return true; - if (e.ShiftPressed) + if (e.ShiftPressed && newBank != EditorSelectionHandler.HIT_BANK_AUTO) { setBank(newBank); updatePrimaryBankState(); @@ -462,7 +475,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline } else { - var item = togglesCollection.ElementAtOrDefault(rightIndex); + var item = togglesCollection.ElementAtOrDefault(rightIndex - 1); if (item is not DrawableTernaryButton button) return base.OnKeyDown(e); @@ -476,18 +489,22 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { switch (key) { - case Key.W: + case Key.Q: index = 0; break; - case Key.E: + case Key.W: index = 1; break; - case Key.R: + case Key.E: index = 2; break; + case Key.R: + index = 3; + break; + default: index = -1; break;