1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-15 12:27:26 +08:00

Merge pull request #29648 from OliBomby/auto-addition2

Add support for the auto sample addition bank in the editor
This commit is contained in:
Dean Herbert 2024-10-25 16:15:52 +09:00 committed by GitHub
commit 47aa2c2bfc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 157 additions and 67 deletions

View File

@ -58,7 +58,7 @@ namespace osu.Game.Rulesets.Catch.Objects
{ {
} }
public sealed override HitSampleInfo With(Optional<string> newName = default, Optional<string> newBank = default, Optional<string?> newSuffix = default, Optional<int> newVolume = default) public sealed override HitSampleInfo With(Optional<string> newName = default, Optional<string> newBank = default, Optional<string?> newSuffix = default, Optional<int> newVolume = default, Optional<bool> newEditorAutoBank = default)
=> new BananaHitSampleInfo(newVolume.GetOr(Volume)); => new BananaHitSampleInfo(newVolume.GetOr(Volume));
public bool Equals(BananaHitSampleInfo? other) public bool Equals(BananaHitSampleInfo? other)

View File

@ -180,7 +180,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
{ {
if (slider == null) return; if (slider == null) return;
sample = new HitSampleInfo("hitwhistle", HitSampleInfo.BANK_SOFT, volume: 70); sample = new HitSampleInfo("hitwhistle", HitSampleInfo.BANK_SOFT, volume: 70, editorAutoBank: false);
slider.Samples.Add(sample.With()); slider.Samples.Add(sample.With());
}); });

View File

@ -60,12 +60,18 @@ namespace osu.Game.Audio
/// </summary> /// </summary>
public int Volume { get; } public int Volume { get; }
public HitSampleInfo(string name, string bank = SampleControlPoint.DEFAULT_BANK, string? suffix = null, int volume = 100) /// <summary>
/// Whether this sample should automatically assign the bank of the normal sample whenever it is set in the editor.
/// </summary>
public bool EditorAutoBank { get; }
public HitSampleInfo(string name, string bank = SampleControlPoint.DEFAULT_BANK, string? suffix = null, int volume = 100, bool editorAutoBank = true)
{ {
Name = name; Name = name;
Bank = bank; Bank = bank;
Suffix = suffix; Suffix = suffix;
Volume = volume; Volume = volume;
EditorAutoBank = editorAutoBank;
} }
/// <summary> /// <summary>
@ -92,9 +98,10 @@ namespace osu.Game.Audio
/// <param name="newBank">An optional new sample bank.</param> /// <param name="newBank">An optional new sample bank.</param>
/// <param name="newSuffix">An optional new lookup suffix.</param> /// <param name="newSuffix">An optional new lookup suffix.</param>
/// <param name="newVolume">An optional new volume.</param> /// <param name="newVolume">An optional new volume.</param>
/// <param name="newEditorAutoBank">An optional new editor auto bank flag.</param>
/// <returns>The new <see cref="HitSampleInfo"/>.</returns> /// <returns>The new <see cref="HitSampleInfo"/>.</returns>
public virtual HitSampleInfo With(Optional<string> newName = default, Optional<string> newBank = default, Optional<string?> newSuffix = default, Optional<int> newVolume = default) public virtual HitSampleInfo With(Optional<string> newName = default, Optional<string> newBank = default, Optional<string?> newSuffix = default, Optional<int> newVolume = default, Optional<bool> newEditorAutoBank = default)
=> new HitSampleInfo(newName.GetOr(Name), newBank.GetOr(Bank), newSuffix.GetOr(Suffix), newVolume.GetOr(Volume)); => new HitSampleInfo(newName.GetOr(Name), newBank.GetOr(Bank), newSuffix.GetOr(Suffix), newVolume.GetOr(Volume), newEditorAutoBank.GetOr(EditorAutoBank));
public virtual bool Equals(HitSampleInfo? other) public virtual bool Equals(HitSampleInfo? other)
=> other != null && Name == other.Name && Bank == other.Bank && Suffix == other.Suffix; => other != null && Name == other.Name && Bank == other.Bank && Suffix == other.Suffix;

View File

@ -539,7 +539,7 @@ namespace osu.Game.Beatmaps.Formats
private string getSampleBank(IList<HitSampleInfo> samples, bool banksOnly = false) private string getSampleBank(IList<HitSampleInfo> samples, bool banksOnly = false)
{ {
LegacySampleBank normalBank = toLegacySampleBank(samples.SingleOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL)?.Bank); 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(); StringBuilder sb = new StringBuilder();

View File

@ -233,7 +233,7 @@ namespace osu.Game.Rulesets.Objects
// Fall back to using the normal sample bank otherwise. // Fall back to using the normal sample bank otherwise.
if (Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL) is HitSampleInfo existingNormal) 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); return new HitSampleInfo(sampleName);
} }

View File

@ -204,8 +204,14 @@ namespace osu.Game.Rulesets.Objects.Legacy
if (stringBank == @"none") if (stringBank == @"none")
stringBank = null; stringBank = null;
string stringAddBank = addBank.ToString().ToLowerInvariant(); string stringAddBank = addBank.ToString().ToLowerInvariant();
if (stringAddBank == @"none") if (stringAddBank == @"none")
{
bankInfo.EditorAutoBank = true;
stringAddBank = null; stringAddBank = null;
}
else
bankInfo.EditorAutoBank = false;
bankInfo.BankForNormal = stringBank; bankInfo.BankForNormal = stringBank;
bankInfo.BankForAdditions = string.IsNullOrEmpty(stringAddBank) ? stringBank : stringAddBank; bankInfo.BankForAdditions = string.IsNullOrEmpty(stringAddBank) ? stringBank : stringAddBank;
@ -477,7 +483,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
if (string.IsNullOrEmpty(bankInfo.Filename)) 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. // 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 // 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))); type != LegacyHitSoundType.None && !type.HasFlag(LegacyHitSoundType.Normal)));
@ -489,13 +495,13 @@ namespace osu.Game.Rulesets.Objects.Legacy
} }
if (type.HasFlag(LegacyHitSoundType.Finish)) 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)) 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)) 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; return soundTypes;
} }
@ -534,6 +540,11 @@ namespace osu.Game.Rulesets.Objects.Legacy
/// </summary> /// </summary>
public int CustomSampleBank; public int CustomSampleBank;
/// <summary>
/// Whether the bank for additions should be inherited from the normal sample in edit.
/// </summary>
public bool EditorAutoBank = true;
public SampleBankInfo Clone() => (SampleBankInfo)MemberwiseClone(); public SampleBankInfo Clone() => (SampleBankInfo)MemberwiseClone();
} }
@ -558,21 +569,21 @@ namespace osu.Game.Rulesets.Objects.Legacy
/// </summary> /// </summary>
public bool BankSpecified; public bool BankSpecified;
public LegacyHitSampleInfo(string name, string? bank = null, int volume = 0, int customSampleBank = 0, bool isLayered = false) 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) : base(name, bank ?? SampleControlPoint.DEFAULT_BANK, customSampleBank >= 2 ? customSampleBank.ToString() : null, volume, editorAutoBank)
{ {
CustomSampleBank = customSampleBank; CustomSampleBank = customSampleBank;
BankSpecified = !string.IsNullOrEmpty(bank); BankSpecified = !string.IsNullOrEmpty(bank);
IsLayered = isLayered; IsLayered = isLayered;
} }
public sealed override HitSampleInfo With(Optional<string> newName = default, Optional<string> newBank = default, Optional<string?> newSuffix = default, Optional<int> newVolume = default) public sealed override HitSampleInfo With(Optional<string> newName = default, Optional<string> newBank = default, Optional<string?> newSuffix = default, Optional<int> newVolume = default, Optional<bool> newEditorAutoBank = default)
=> With(newName, newBank, newVolume); => With(newName, newBank, newVolume, newEditorAutoBank);
public virtual LegacyHitSampleInfo With(Optional<string> newName = default, Optional<string> newBank = default, Optional<int> newVolume = default, public virtual LegacyHitSampleInfo With(Optional<string> newName = default, Optional<string> newBank = default, Optional<int> newVolume = default, Optional<bool> newEditorAutoBank = default,
Optional<int> newCustomSampleBank = default, Optional<int> newCustomSampleBank = default,
Optional<bool> newIsLayered = default) Optional<bool> 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) public bool Equals(LegacyHitSampleInfo? other)
// The additions to equality checks here are *required* to ensure that pooling works correctly. // 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) Path.ChangeExtension(Filename, null)
}; };
public sealed override LegacyHitSampleInfo With(Optional<string> newName = default, Optional<string> newBank = default, Optional<int> newVolume = default, public sealed override LegacyHitSampleInfo With(Optional<string> newName = default, Optional<string> newBank = default, Optional<int> newVolume = default, Optional<bool> newEditorAutoBank = default,
Optional<int> newCustomSampleBank = default, Optional<int> newCustomSampleBank = default,
Optional<bool> newIsLayered = default) Optional<bool> newIsLayered = default)
=> new FileHitSampleInfo(Filename, newVolume.GetOr(Volume)); => new FileHitSampleInfo(Filename, newVolume.GetOr(Volume));

View File

@ -4,8 +4,10 @@
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays; using osu.Game.Overlays;
@ -14,7 +16,7 @@ using osuTK.Graphics;
namespace osu.Game.Screens.Edit.Components.TernaryButtons namespace osu.Game.Screens.Edit.Components.TernaryButtons
{ {
public partial class DrawableTernaryButton : OsuButton public partial class DrawableTernaryButton : OsuButton, IHasTooltip
{ {
private Color4 defaultBackgroundColour; private Color4 defaultBackgroundColour;
private Color4 defaultIconColour; private Color4 defaultIconColour;
@ -58,12 +60,16 @@ namespace osu.Game.Screens.Edit.Components.TernaryButtons
base.LoadComplete(); base.LoadComplete();
Button.Bindable.BindValueChanged(_ => updateSelectionState(), true); Button.Bindable.BindValueChanged(_ => updateSelectionState(), true);
Button.Enabled.BindTo(Enabled);
Action = onAction; Action = onAction;
} }
private void onAction() private void onAction()
{ {
if (!Button.Enabled.Value)
return;
Button.Toggle(); Button.Toggle();
} }
@ -98,5 +104,7 @@ namespace osu.Game.Screens.Edit.Components.TernaryButtons
Anchor = Anchor.CentreLeft, Anchor = Anchor.CentreLeft,
X = 40f X = 40f
}; };
public LocalisableString TooltipText => Button.Tooltip;
} }
} }

View File

@ -12,6 +12,8 @@ namespace osu.Game.Screens.Edit.Components.TernaryButtons
{ {
public readonly Bindable<TernaryState> Bindable; public readonly Bindable<TernaryState> Bindable;
public readonly Bindable<bool> Enabled = new Bindable<bool>(true);
public readonly string Description; public readonly string Description;
/// <summary> /// <summary>
@ -19,6 +21,8 @@ namespace osu.Game.Screens.Edit.Components.TernaryButtons
/// </summary> /// </summary>
public readonly Func<Drawable>? CreateIcon; public readonly Func<Drawable>? CreateIcon;
public string Tooltip { get; set; } = string.Empty;
public TernaryButton(Bindable<TernaryState> bindable, string description, Func<Drawable>? createIcon = null) public TernaryButton(Bindable<TernaryState> bindable, string description, Func<Drawable>? createIcon = null)
{ {
Bindable = bindable; Bindable = bindable;

View File

@ -68,6 +68,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
SampleBankTernaryStates = createSampleBankTernaryButtons(SelectionHandler.SelectionBankStates).ToArray(); SampleBankTernaryStates = createSampleBankTernaryButtons(SelectionHandler.SelectionBankStates).ToArray();
SampleAdditionBankTernaryStates = createSampleBankTernaryButtons(SelectionHandler.SelectionAdditionBankStates).ToArray(); SampleAdditionBankTernaryStates = createSampleBankTernaryButtons(SelectionHandler.SelectionAdditionBankStates).ToArray();
SelectionHandler.AutoSelectionBankEnabled.BindValueChanged(_ => updateAutoBankTernaryButtonTooltip(), true);
SelectionHandler.SelectionAdditionBanksEnabled.BindValueChanged(_ => updateAdditionBankTernaryButtonTooltips(), true);
AddInternal(new DrawableRulesetDependenciesProvidingContainer(Composer.Ruleset) AddInternal(new DrawableRulesetDependenciesProvidingContainer(Composer.Ruleset)
{ {
Child = placementBlueprintContainer Child = placementBlueprintContainer
@ -288,6 +291,26 @@ namespace osu.Game.Screens.Edit.Compose.Components
return null; return null;
} }
private void updateAutoBankTernaryButtonTooltip()
{
bool enabled = SelectionHandler.AutoSelectionBankEnabled.Value;
var autoBankButton = SampleBankTernaryStates.Single(t => t.Bindable == SelectionHandler.SelectionBankStates[EditorSelectionHandler.HIT_BANK_AUTO]);
autoBankButton.Enabled.Value = enabled;
autoBankButton.Tooltip = !enabled ? "Auto normal bank can only be used during hit object placement" : string.Empty;
}
private void updateAdditionBankTernaryButtonTooltips()
{
bool enabled = SelectionHandler.SelectionAdditionBanksEnabled.Value;
foreach (var ternaryButton in SampleAdditionBankTernaryStates)
{
ternaryButton.Enabled.Value = enabled;
ternaryButton.Tooltip = !enabled ? "Add an addition sample first to be able to set a bank" : string.Empty;
}
}
#region Placement #region Placement
/// <summary> /// <summary>

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq; using System.Linq;
using Humanizer; using Humanizer;
using osu.Framework.Allocation; using osu.Framework.Allocation;
@ -10,7 +11,6 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
@ -37,7 +37,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
// bring in updates from selection changes // bring in updates from selection changes
EditorBeatmap.HitObjectUpdated += _ => Scheduler.AddOnce(UpdateTernaryStates); EditorBeatmap.HitObjectUpdated += _ => Scheduler.AddOnce(UpdateTernaryStates);
SelectedItems.CollectionChanged += (_, _) => Scheduler.AddOnce(UpdateTernaryStates); SelectedItems.CollectionChanged += onSelectedItemsChanged;
} }
protected override void DeleteItems(IEnumerable<HitObject> items) => EditorBeatmap.RemoveRange(items); protected override void DeleteItems(IEnumerable<HitObject> items) => EditorBeatmap.RemoveRange(items);
@ -64,6 +64,16 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// </summary> /// </summary>
public readonly Dictionary<string, Bindable<TernaryState>> SelectionAdditionBankStates = new Dictionary<string, Bindable<TernaryState>>(); public readonly Dictionary<string, Bindable<TernaryState>> SelectionAdditionBankStates = new Dictionary<string, Bindable<TernaryState>>();
/// <summary>
/// Whether there is no selection and the auto <see cref="SelectionBankStates"/> can be used.
/// </summary>
public readonly Bindable<bool> AutoSelectionBankEnabled = new Bindable<bool>();
/// <summary>
/// Whether the selection contains any addition samples and the <see cref="SelectionAdditionBankStates"/> can be used.
/// </summary>
public readonly Bindable<bool> SelectionAdditionBanksEnabled = new Bindable<bool>();
/// <summary> /// <summary>
/// Set up ternary state bindables and bind them to selection/hitobject changes (in both directions) /// Set up ternary state bindables and bind them to selection/hitobject changes (in both directions)
/// </summary> /// </summary>
@ -153,10 +163,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
} }
else 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. // 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). // 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))) if (SelectedItems.SelectMany(enumerateAllSamples).All(h => h.All(o => o.Name == HitSampleInfo.HIT_NORMAL)))
@ -164,8 +170,16 @@ namespace osu.Game.Screens.Edit.Compose.Components
// Never remove a sample bank. // Never remove a sample bank.
// These are basically radio buttons, not toggles. // 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))) if (bankName == HIT_BANK_AUTO)
bindable.Value = TernaryState.True; {
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; break;
@ -183,14 +197,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
} }
else 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 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))) if (SelectedItems.SelectMany(enumerateAllSamples).All(h => h.All(o => o.Name == HitSampleInfo.HIT_NORMAL)))
{ {
@ -208,9 +214,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
SelectionAdditionBankStates[bankName] = bindable; SelectionAdditionBankStates[bankName] = bindable;
} }
// start with normal selected. resetTernaryStates();
SelectionBankStates[SampleControlPoint.DEFAULT_BANK].Value = TernaryState.True;
SelectionAdditionBankStates[SampleControlPoint.DEFAULT_BANK].Value = TernaryState.True;
foreach (string sampleName in HitSampleInfo.AllAdditions) foreach (string sampleName in HitSampleInfo.AllAdditions)
{ {
@ -252,12 +256,21 @@ namespace osu.Game.Screens.Edit.Compose.Components
}; };
} }
private void resetTernaryStates()
{
AutoSelectionBankEnabled.Value = true;
SelectionAdditionBanksEnabled.Value = true;
SelectionBankStates[HIT_BANK_AUTO].Value = TernaryState.True;
SelectionAdditionBankStates[HIT_BANK_AUTO].Value = TernaryState.True;
}
/// <summary> /// <summary>
/// Called when context menu ternary states may need to be recalculated (selection changed or hitobject updated). /// Called when context menu ternary states may need to be recalculated (selection changed or hitobject updated).
/// </summary> /// </summary>
protected virtual void UpdateTernaryStates() protected virtual void UpdateTernaryStates()
{ {
SelectionNewComboState.Value = GetStateFromSelection(SelectedItems.OfType<IHasComboInformation>(), h => h.NewCombo); SelectionNewComboState.Value = GetStateFromSelection(SelectedItems.OfType<IHasComboInformation>(), h => h.NewCombo);
AutoSelectionBankEnabled.Value = SelectedItems.Count == 0;
var samplesInSelection = SelectedItems.SelectMany(enumerateAllSamples).ToArray(); var samplesInSelection = SelectedItems.SelectMany(enumerateAllSamples).ToArray();
@ -271,12 +284,23 @@ namespace osu.Game.Screens.Edit.Compose.Components
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 => h.Bank == bankName);
} }
SelectionAdditionBanksEnabled.Value = samplesInSelection.SelectMany(s => s).Any(o => o.Name != HitSampleInfo.HIT_NORMAL);
foreach ((string bankName, var bindable) in SelectionAdditionBankStates) 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));
} }
} }
private void onSelectedItemsChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
// Reset the ternary states when the selection is cleared.
if (e.OldStartingIndex >= 0 && e.NewStartingIndex < 0)
Scheduler.AddOnce(resetTernaryStates);
else
Scheduler.AddOnce(UpdateTernaryStates);
}
private IEnumerable<IList<HitSampleInfo>> enumerateAllSamples(HitObject hitObject) private IEnumerable<IList<HitSampleInfo>> enumerateAllSamples(HitObject hitObject)
{ {
yield return hitObject.Samples; yield return hitObject.Samples;
@ -337,33 +361,29 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// <param name="bankName">The name of the sample bank.</param> /// <param name="bankName">The name of the sample bank.</param>
public void SetSampleAdditionBank(string bankName) public void SetSampleAdditionBank(string bankName)
{ {
bool hasRelevantBank(HitObject hitObject) bool hasRelevantBank(HitObject hitObject) =>
{ bankName == HIT_BANK_AUTO
bool result = hitObject.Samples.Where(o => o.Name != HitSampleInfo.HIT_NORMAL).All(s => s.Bank == bankName); ? 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 (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;
}
if (SelectedItems.All(hasRelevantBank)) if (SelectedItems.All(hasRelevantBank))
return; return;
EditorBeatmap.PerformOnSelection(h => 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; 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) if (h is IHasRepeats hasRepeats)
{ {
for (int i = 0; i < hasRepeats.NodeSamples.Count; ++i) 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); EditorBeatmap.Update(h);
@ -407,9 +427,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
var hitSample = h.CreateHitSampleInfo(sampleName); var hitSample = h.CreateHitSampleInfo(sampleName);
string? existingAdditionBank = node.FirstOrDefault(s => s.Name != HitSampleInfo.HIT_NORMAL)?.Bank; HitSampleInfo? existingAddition = node.FirstOrDefault(s => s.Name != HitSampleInfo.HIT_NORMAL);
if (existingAdditionBank != null) if (existingAddition != null)
hitSample = hitSample.With(newBank: existingAdditionBank); hitSample = hitSample.With(newBank: existingAddition.Bank, newEditorAutoBank: existingAddition.EditorAutoBank);
node.Add(hitSample); node.Add(hitSample);
} }

View File

@ -107,7 +107,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
public static string? GetAdditionBankValue(IEnumerable<HitSampleInfo> samples) public static string? GetAdditionBankValue(IEnumerable<HitSampleInfo> 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<HitSampleInfo> samples) public static int GetVolumeValue(ICollection<HitSampleInfo> samples)
@ -320,7 +324,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{ {
for (int i = 0; i < relevantSamples.Count; i++) 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); relevantSamples[i] = relevantSamples[i].With(newBank: newBank);
} }
@ -331,11 +335,20 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{ {
updateAllRelevantSamples((_, relevantSamples) => updateAllRelevantSamples((_, relevantSamples) =>
{ {
string normalBank = relevantSamples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL)?.Bank ?? HitSampleInfo.BANK_SOFT;
for (int i = 0; i < relevantSamples.Count; i++) 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; selectionSampleStates[sampleName] = bindable;
} }
banks.AddRange(HitSampleInfo.AllBanks); banks.AddRange(HitSampleInfo.AllBanks.Prepend(EditorSelectionHandler.HIT_BANK_AUTO));
} }
private void updateTernaryStates() private void updateTernaryStates()
@ -448,7 +461,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
if (string.IsNullOrEmpty(newBank)) if (string.IsNullOrEmpty(newBank))
return true; return true;
if (e.ShiftPressed) if (e.ShiftPressed && newBank != EditorSelectionHandler.HIT_BANK_AUTO)
{ {
setBank(newBank); setBank(newBank);
updatePrimaryBankState(); updatePrimaryBankState();
@ -462,7 +475,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
} }
else else
{ {
var item = togglesCollection.ElementAtOrDefault(rightIndex); var item = togglesCollection.ElementAtOrDefault(rightIndex - 1);
if (item is not DrawableTernaryButton button) return base.OnKeyDown(e); if (item is not DrawableTernaryButton button) return base.OnKeyDown(e);
@ -476,18 +489,22 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{ {
switch (key) switch (key)
{ {
case Key.W: case Key.Q:
index = 0; index = 0;
break; break;
case Key.E: case Key.W:
index = 1; index = 1;
break; break;
case Key.R: case Key.E:
index = 2; index = 2;
break; break;
case Key.R:
index = 3;
break;
default: default:
index = -1; index = -1;
break; break;