diff --git a/osu.Game/Graphics/UserInterfaceV2/LabelledDropdown.cs b/osu.Game/Graphics/UserInterfaceV2/LabelledDropdown.cs index a37267014f..5dd9db7506 100644 --- a/osu.Game/Graphics/UserInterfaceV2/LabelledDropdown.cs +++ b/osu.Game/Graphics/UserInterfaceV2/LabelledDropdown.cs @@ -31,6 +31,11 @@ namespace osu.Game.Graphics.UserInterfaceV2 d.RelativeSizeAxes = Axes.X; }); - protected virtual OsuDropdown CreateDropdown() => new OsuDropdown(); + protected virtual OsuDropdown CreateDropdown() => new Dropdown(); + + private partial class Dropdown : OsuDropdown + { + protected override DropdownMenu CreateMenu() => base.CreateMenu().With(menu => menu.MaxHeight = 200); + } } } diff --git a/osu.Game/Screens/Edit/Components/TernaryButtons/DrawableTernaryButton.cs b/osu.Game/Screens/Edit/Components/TernaryButtons/DrawableTernaryButton.cs index 7b36b5f957..9cc962fe8f 100644 --- a/osu.Game/Screens/Edit/Components/TernaryButtons/DrawableTernaryButton.cs +++ b/osu.Game/Screens/Edit/Components/TernaryButtons/DrawableTernaryButton.cs @@ -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; diff --git a/osu.Game/Screens/Edit/Components/TernaryButtons/SampleSetTernaryButton.cs b/osu.Game/Screens/Edit/Components/TernaryButtons/SampleSetTernaryButton.cs new file mode 100644 index 0000000000..ae26024650 --- /dev/null +++ b/osu.Game/Screens/Edit/Components/TernaryButtons/SampleSetTernaryButton.cs @@ -0,0 +1,85 @@ +// Copyright (c) ppy Pty Ltd . 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(); + }; + } + } +} diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index bf31ea4e2e..6fd54da360 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -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 bank = null!; private LabelledDropdown additionBank = null!; + private FillFlowContainer? sampleSetsFlow; + private LabelledDropdown? sampleSetDropdown; private IndeterminateSliderWithTextBoxInput 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("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 + { + 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(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 activeSets = new HashSet(); + + 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().ToArray(); + demoSample.Play(); + } + /// /// Applies the given update action on all samples of /// 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; diff --git a/osu.Game/Screens/Edit/EditorBeatmapSkin.cs b/osu.Game/Screens/Edit/EditorBeatmapSkin.cs index 07fa1cb49c..315ddf8f95 100644 --- a/osu.Game/Screens/Edit/EditorBeatmapSkin.cs +++ b/osu.Game/Screens/Edit/EditorBeatmapSkin.cs @@ -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 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 indices = new HashSet(); + + 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); diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index 07902106ef..d985c33f54 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -38,7 +38,7 @@ namespace osu.Game.Skinning /// /// A sample store which can be used to perform user file lookups for this skin. /// - protected ISampleStore? Samples { get; } + protected internal ISampleStore? Samples { get; } public readonly Live SkinInfo;