1
0
mirror of https://github.com/ppy/osu.git synced 2026-06-09 20:54:47 +08:00

Add sample set selection controls to sample popovers

- Below 20 custom sample sets, they are shown as ternary buttons.
- Above 20 custom sample sets, they are shown in a dropdown (yes there
  are actual cases of this as I've been informed by the NAT; one example
  being https://osu.ppy.sh/beatmapsets/1018061#osu/2197383)

As a bonus, to make determining what the heck is actually changing when
adjusting these controls, the full set of applicable sounds now plays on
adding/removing additions, changing their banks, as well as changing the
custom set (if any).

For now there are no user-facing controls to add the samples to the map
yourself, you have to know how to name the `.wav`s and edit-externally
them in yourself. *For now.*
This commit is contained in:
Bartłomiej Dach
2025-10-15 11:00:57 +02:00
Unverified
parent 1f2f86b016
commit e2681c4163
6 changed files with 254 additions and 7 deletions
@@ -31,6 +31,11 @@ namespace osu.Game.Graphics.UserInterfaceV2
d.RelativeSizeAxes = Axes.X;
});
protected virtual OsuDropdown<TItem> CreateDropdown() => new OsuDropdown<TItem>();
protected virtual OsuDropdown<TItem> CreateDropdown() => new Dropdown();
private partial class Dropdown : OsuDropdown<TItem>
{
protected override DropdownMenu CreateMenu() => base.CreateMenu().With(menu => menu.MaxHeight = 200);
}
}
}
@@ -49,7 +49,8 @@ namespace osu.Game.Screens.Edit.Components.TernaryButtons
public Drawable Icon { get; private set; } = null!;
public DrawableTernaryButton()
public DrawableTernaryButton(HoverSampleSet? hoverSampleSet = HoverSampleSet.Button)
: base(hoverSampleSet)
{
RelativeSizeAxes = Axes.X;
}
@@ -79,10 +80,10 @@ namespace osu.Game.Screens.Edit.Components.TernaryButtons
current.BindValueChanged(_ => updateSelectionState(), true);
Action = onAction;
Action = OnAction;
}
private void onAction()
protected void OnAction()
{
if (!Enabled.Value)
return;
@@ -0,0 +1,85 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Audio;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Skinning;
namespace osu.Game.Screens.Edit.Components.TernaryButtons
{
public partial class SampleSetTernaryButton : DrawableTernaryButton
{
public EditorBeatmapSkin.SampleSet SampleSet { get; }
public ISampleInfo[] DemoSamples
{
get => demoSample.Samples;
set => demoSample.Samples = value;
}
private readonly SkinnableSound demoSample;
public SampleSetTernaryButton(EditorBeatmapSkin.SampleSet sampleSet)
: base(null)
{
SampleSet = sampleSet;
CreateIcon = () => sampleSet.SampleSetIndex == 0
? new SpriteIcon { Icon = OsuIcon.SkinA }
: new Container
{
Child = new OsuSpriteText
{
Text = sampleSet.SampleSetIndex.ToString(),
Font = OsuFont.Style.Body.With(weight: FontWeight.Bold),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}
};
switch (sampleSet.SampleSetIndex)
{
case 0:
RelativeSizeAxes = Axes.X;
Width = 1;
break;
default:
RelativeSizeAxes = Axes.None;
Width = Height;
break;
}
demoSample = new SkinnableSound();
}
[BackgroundDependencyLoader]
private void load(EditorBeatmap editorBeatmap)
{
AddRangeInternal(new Drawable[]
{
new HoverSounds(HoverSampleSet.Button),
new EditorSkinProvidingContainer(editorBeatmap)
{
Child = demoSample,
}
});
}
protected override void LoadComplete()
{
base.LoadComplete();
Action = () =>
{
OnAction();
demoSample?.Play();
};
}
}
}
@@ -20,10 +20,11 @@ using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Rulesets.Objects;
using osu.Game.Screens.Edit.Components.TernaryButtons;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Screens.Edit.Components.TernaryButtons;
using osu.Game.Screens.Edit.Timing;
using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
using osuTK.Input;
@@ -189,7 +190,10 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private LabelledDropdown<string> bank = null!;
private LabelledDropdown<string> additionBank = null!;
private FillFlowContainer<SampleSetTernaryButton>? sampleSetsFlow;
private LabelledDropdown<EditorBeatmapSkin.SampleSet>? sampleSetDropdown;
private IndeterminateSliderWithTextBoxInput<int> volume = null!;
private SkinnableSound demoSample = null!;
private FillFlowContainer togglesCollection = null!;
@@ -244,7 +248,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
Direction = FillDirection.Vertical,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(0, 10),
Children = new Drawable[]
Children = new[]
{
togglesCollection = new FillFlowContainer
{
@@ -263,12 +267,17 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
Label = "Addition Bank",
Items = HitSampleInfo.ALL_BANKS,
},
createSampleSetContent(),
volume = new IndeterminateSliderWithTextBoxInput<int>("Volume", new BindableInt(100)
{
MinValue = DrawableHitObject.MINIMUM_SAMPLE_VOLUME,
MaxValue = 100,
})
}
},
new EditorSkinProvidingContainer(beatmap)
{
Child = demoSample = new SkinnableSound()
}
};
@@ -292,6 +301,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
setBank(val.NewValue);
updatePrimaryBankState();
playDemoSample();
});
updateAdditionBankState();
@@ -302,8 +312,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
setAdditionBank(val.NewValue);
updateAdditionBankState();
playDemoSample();
});
updateSampleSetState();
volume.Current.BindValueChanged(val =>
{
if (val.NewValue != null)
@@ -315,6 +328,58 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
togglesCollection.AddRange(createTernaryButtons());
}
private Drawable createSampleSetContent()
{
if (beatmap.BeatmapSkin == null)
return Empty();
var sampleSets = beatmap.BeatmapSkin.GetAvailableSampleSets().ToList();
if (sampleSets.Count == 0)
return Empty();
sampleSets.Insert(0, new EditorBeatmapSkin.SampleSet(0, "User skin"));
if (sampleSets.Count < 20)
{
sampleSetsFlow = new FillFlowContainer<SampleSetTernaryButton>
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(5),
ChildrenEnumerable = sampleSets.Select(set => new SampleSetTernaryButton(set) { Description = set.Name }),
};
foreach (var ternary in sampleSetsFlow)
{
ternary.Current.BindValueChanged(val =>
{
if (val.NewValue == TernaryState.True)
setSampleSet(ternary.SampleSet);
updateSampleSetState();
playDemoSample();
});
}
return sampleSetsFlow;
}
sampleSetDropdown = new LabelledDropdown<EditorBeatmapSkin.SampleSet>(padded: false)
{
Label = "Sample Set",
Items = sampleSets,
};
sampleSetDropdown.Current.BindValueChanged(val =>
{
setSampleSet(val.NewValue);
updateSampleSetState();
playDemoSample();
});
return sampleSetDropdown;
}
private string? getCommonBank() => allRelevantSamples.Select(h => GetBankValue(h.samples)).Distinct().Count() == 1
? GetBankValue(allRelevantSamples.First().samples)
: null;
@@ -347,6 +412,40 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
additionBank.Hide();
}
private void updateSampleSetState()
{
HashSet<int> activeSets = new HashSet<int>();
foreach (var sample in allRelevantSamples.SelectMany(h => h.samples))
{
if (sample.Suffix == null)
activeSets.Add(sample.UseBeatmapSamples ? 1 : 0);
else if (int.TryParse(sample.Suffix, out int suffix))
activeSets.Add(suffix);
}
if (sampleSetsFlow != null)
{
var onState = activeSets.Count > 1 ? TernaryState.Indeterminate : TernaryState.True;
foreach (var ternary in sampleSetsFlow)
ternary.Current.Value = activeSets.Contains(ternary.SampleSet.SampleSetIndex) ? onState : TernaryState.False;
}
if (sampleSetDropdown != null)
{
sampleSetDropdown.Current.Value = activeSets.Count == 1
? sampleSetDropdown.Items.Single(i => i.SampleSetIndex == activeSets.Single())
: new EditorBeatmapSkin.SampleSet(-1, "(multiple)");
}
}
private void playDemoSample()
{
demoSample.Samples = allRelevantSamples.First().samples.Cast<ISampleInfo>().ToArray();
demoSample.Play();
}
/// <summary>
/// Applies the given update action on all samples of <see cref="allRelevantSamples"/>
/// and invokes the necessary update notifiers for the beatmap and hit objects.
@@ -400,6 +499,19 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
});
}
private void setSampleSet(EditorBeatmapSkin.SampleSet newSampleSet)
{
updateAllRelevantSamples((_, relevantSamples) =>
{
for (int i = 0; i < relevantSamples.Count; i++)
{
relevantSamples[i] = relevantSamples[i].With(
newSuffix: newSampleSet.SampleSetIndex >= 2 ? newSampleSet.SampleSetIndex.ToString() : null,
newUseBeatmapSamples: newSampleSet.SampleSetIndex >= 1);
}
});
}
private void setVolume(int newVolume)
{
updateAllRelevantSamples((_, relevantSamples) =>
@@ -438,6 +550,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
addHitSample(sampleName);
break;
}
playDemoSample();
};
selectionSampleStates[sampleName] = bindable;
@@ -3,6 +3,8 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@@ -61,6 +63,46 @@ namespace osu.Game.Screens.Edit
invokeSkinChanged();
}
public record SampleSet(int SampleSetIndex, string Name)
{
public SampleSet(int sampleSetIndex)
: this(sampleSetIndex, $@"Custom #{sampleSetIndex}")
{
}
public override string ToString() => Name;
}
public IEnumerable<SampleSet> GetAvailableSampleSets()
{
string[] possibleSounds = [HitSampleInfo.HIT_NORMAL, ..HitSampleInfo.ALL_ADDITIONS];
string[] possibleBanks = HitSampleInfo.ALL_BANKS;
string[] possiblePrefixes = possibleSounds.SelectMany(sound => possibleBanks.Select(bank => $@"{bank}-{sound}")).ToArray();
HashSet<int> indices = new HashSet<int>();
if (Skin.Samples != null)
{
foreach (string sample in Skin.Samples.GetAvailableResources())
{
foreach (string possiblePrefix in possiblePrefixes)
{
if (!sample.StartsWith(possiblePrefix, StringComparison.InvariantCultureIgnoreCase))
continue;
string indexString = Path.GetFileNameWithoutExtension(sample)[possiblePrefix.Length..];
if (string.IsNullOrEmpty(indexString))
indices.Add(1);
if (int.TryParse(indexString, out int index))
indices.Add(index);
}
}
}
return indices.OrderBy(i => i).Select(i => new SampleSet(i));
}
#region Delegated ISkin implementation
public Drawable? GetDrawableComponent(ISkinComponentLookup lookup) => Skin.GetDrawableComponent(lookup);
+1 -1
View File
@@ -38,7 +38,7 @@ namespace osu.Game.Skinning
/// <summary>
/// A sample store which can be used to perform user file lookups for this skin.
/// </summary>
protected ISampleStore? Samples { get; }
protected internal ISampleStore? Samples { get; }
public readonly Live<SkinInfo> SkinInfo;