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:
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user