Skip to content

Commit

Permalink
Merge pull request #23443 from OliBomby/edit-nodesample
Browse files Browse the repository at this point in the history
Make NodeSamples editable
  • Loading branch information
peppy authored Jun 18, 2024
2 parents b535f7c + 869cd40 commit 316125d
Show file tree
Hide file tree
Showing 12 changed files with 547 additions and 56 deletions.
2 changes: 2 additions & 0 deletions osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ protected override void CreateNestedHitObjects(CancellationToken cancellationTok
{
base.CreateNestedHitObjects(cancellationToken);

this.PopulateNodeSamples();

var dropletSamples = Samples.Select(s => s.With(@"slidertick")).ToList();

int nodeIndex = 0;
Expand Down
1 change: 1 addition & 0 deletions osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ public void TestChangeSamplesWithNoNodeSamples()
{
slider = (DrawableSlider)createSlider(repeats: 1);
Add(slider);
slider.HitObject.NodeSamples.Clear();
});

AddStep("change samples", () => slider.HitObject.Samples = new[]
Expand Down
2 changes: 2 additions & 0 deletions osu.Game.Rulesets.Osu/Objects/Slider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,8 @@ private void updateNestedPositions()

protected void UpdateNestedSamples()
{
this.PopulateNodeSamples();

// TODO: remove this when guaranteed sort is present for samples (https://github.com/ppy/osu/issues/1933)
HitSampleInfo tickSample = (Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL) ?? Samples.FirstOrDefault())?.With("slidertick");

Expand Down
160 changes: 155 additions & 5 deletions osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,12 @@
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Screens.Edit.Components.TernaryButtons;
using osu.Game.Screens.Edit.Compose.Components.Timeline;
using osu.Game.Screens.Edit.Timing;
using osu.Game.Tests.Beatmaps;
Expand Down Expand Up @@ -79,10 +82,10 @@ public void TestAddSampleAddition()
}

[Test]
public void TestPopoverHasFocus()
public void TestPopoverHasNoFocus()
{
clickSamplePiece(0);
samplePopoverHasFocus();
samplePopoverHasNoFocus();
}

[Test]
Expand Down Expand Up @@ -226,6 +229,84 @@ public void TestPopoverMultipleSelectionWithDifferentSampleBank()
samplePopoverHasSingleBank(HitSampleInfo.BANK_NORMAL);
}

[Test]
public void TestPopoverAddSampleAddition()
{
clickSamplePiece(0);

setBankViaPopover(HitSampleInfo.BANK_SOFT);
hitObjectHasSampleBank(0, HitSampleInfo.BANK_SOFT);

toggleAdditionViaPopover(0);

hitObjectHasSampleBank(0, HitSampleInfo.BANK_SOFT);
hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE);

setAdditionBankViaPopover(HitSampleInfo.BANK_DRUM);

hitObjectHasSampleNormalBank(0, HitSampleInfo.BANK_SOFT);
hitObjectHasSampleAdditionBank(0, HitSampleInfo.BANK_DRUM);

toggleAdditionViaPopover(0);

hitObjectHasSampleBank(0, HitSampleInfo.BANK_SOFT);
hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL);
}

[Test]
public void TestNodeSamplePopover()
{
AddStep("add slider", () =>
{
EditorBeatmap.Clear();
EditorBeatmap.Add(new Slider
{
Position = new Vector2(256, 256),
StartTime = 0,
Path = new SliderPath(new[] { new PathControlPoint(Vector2.Zero), new PathControlPoint(new Vector2(250, 0)) }),
Samples =
{
new HitSampleInfo(HitSampleInfo.HIT_NORMAL)
},
NodeSamples =
{
new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) },
new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) },
}
});
});

clickNodeSamplePiece(0, 1);

setBankViaPopover(HitSampleInfo.BANK_SOFT);
hitObjectNodeHasSampleBank(0, 0, HitSampleInfo.BANK_NORMAL);
hitObjectNodeHasSampleBank(0, 1, HitSampleInfo.BANK_SOFT);

toggleAdditionViaPopover(0);

hitObjectNodeHasSampleBank(0, 0, HitSampleInfo.BANK_NORMAL);
hitObjectNodeHasSampleBank(0, 1, HitSampleInfo.BANK_SOFT);
hitObjectNodeHasSamples(0, 0, HitSampleInfo.HIT_NORMAL);
hitObjectNodeHasSamples(0, 1, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE);

setAdditionBankViaPopover(HitSampleInfo.BANK_DRUM);

hitObjectNodeHasSampleBank(0, 0, HitSampleInfo.BANK_NORMAL);
hitObjectNodeHasSampleNormalBank(0, 1, HitSampleInfo.BANK_SOFT);
hitObjectNodeHasSampleAdditionBank(0, 1, HitSampleInfo.BANK_DRUM);

toggleAdditionViaPopover(0);

hitObjectNodeHasSampleBank(0, 1, HitSampleInfo.BANK_SOFT);
hitObjectNodeHasSamples(0, 0, HitSampleInfo.HIT_NORMAL);
hitObjectNodeHasSamples(0, 1, HitSampleInfo.HIT_NORMAL);

setVolumeViaPopover(10);

hitObjectNodeHasSampleVolume(0, 0, 100);
hitObjectNodeHasSampleVolume(0, 1, 10);
}

[Test]
public void TestHotkeysMultipleSelectionWithSameSampleBank()
{
Expand Down Expand Up @@ -329,13 +410,21 @@ private void clickSamplePiece(int objectIndex) => AddStep($"click {objectIndex.T
InputManager.Click(MouseButton.Left);
});

private void samplePopoverHasFocus() => AddUntilStep("sample popover textbox focused", () =>
private void clickNodeSamplePiece(int objectIndex, int nodeIndex) => AddStep($"click {objectIndex.ToOrdinalWords()} object {nodeIndex.ToOrdinalWords()} node sample piece", () =>
{
var samplePiece = this.ChildrenOfType<NodeSamplePointPiece>().Where(piece => piece.HitObject == EditorBeatmap.HitObjects.ElementAt(objectIndex)).ToArray()[nodeIndex];

InputManager.MoveMouseTo(samplePiece);
InputManager.Click(MouseButton.Left);
});

private void samplePopoverHasNoFocus() => AddUntilStep("sample popover textbox not focused", () =>
{
var popover = this.ChildrenOfType<SamplePointPiece.SampleEditPopover>().SingleOrDefault();
var slider = popover?.ChildrenOfType<IndeterminateSliderWithTextBoxInput<int>>().Single();
var textbox = slider?.ChildrenOfType<OsuTextBox>().Single();

return textbox?.HasFocus == true;
return textbox?.HasFocus == false;
});

private void samplePopoverHasSingleVolume(int volume) => AddUntilStep($"sample popover has volume {volume}", () =>
Expand Down Expand Up @@ -372,7 +461,6 @@ private void samplePopoverHasIndeterminateBank() => AddUntilStep("sample popover

private void dismissPopover()
{
AddStep("unfocus textbox", () => InputManager.Key(Key.Escape));
AddStep("dismiss popover", () => InputManager.Key(Key.Escape));
AddUntilStep("wait for dismiss", () => !this.ChildrenOfType<SamplePointPiece.SampleEditPopover>().Any(popover => popover.IsPresent));
}
Expand All @@ -390,6 +478,12 @@ private void hitObjectHasSampleVolume(int objectIndex, int volume) => AddAssert(
return h.Samples.All(o => o.Volume == volume);
});

private void hitObjectNodeHasSampleVolume(int objectIndex, int nodeIndex, int volume) => AddAssert($"{objectIndex.ToOrdinalWords()} object {nodeIndex.ToOrdinalWords()} node has volume {volume}", () =>
{
var h = EditorBeatmap.HitObjects.ElementAt(objectIndex) as IHasRepeats;
return h is not null && h.NodeSamples[nodeIndex].All(o => o.Volume == volume);
});

private void setBankViaPopover(string bank) => AddStep($"set bank {bank} via popover", () =>
{
var popover = this.ChildrenOfType<SamplePointPiece.SampleEditPopover>().Single();
Expand All @@ -401,6 +495,26 @@ private void setBankViaPopover(string bank) => AddStep($"set bank {bank} via pop
InputManager.Key(Key.Enter);
});

private void setAdditionBankViaPopover(string bank) => AddStep($"set addition bank {bank} via popover", () =>
{
var popover = this.ChildrenOfType<SamplePointPiece.SampleEditPopover>().Single();
var textBox = popover.ChildrenOfType<LabelledTextBox>().ToArray()[1];
textBox.Current.Value = bank;
// force a commit via keyboard.
// this is needed when testing attempting to set empty bank - which should revert to the previous value, but only on commit.
((IFocusManager)InputManager).ChangeFocus(textBox);
InputManager.Key(Key.Enter);
});

private void toggleAdditionViaPopover(int index) => AddStep($"toggle addition {index} via popover", () =>
{
var popover = this.ChildrenOfType<SamplePointPiece.SampleEditPopover>().First();
var ternaryButton = popover.ChildrenOfType<DrawableTernaryButton>().ToArray()[index];
InputManager.MoveMouseTo(ternaryButton);
InputManager.PressButton(MouseButton.Left);
InputManager.ReleaseButton(MouseButton.Left);
});

private void hitObjectHasSamples(int objectIndex, params string[] samples) => AddAssert($"{objectIndex.ToOrdinalWords()} has samples {string.Join(',', samples)}", () =>
{
var h = EditorBeatmap.HitObjects.ElementAt(objectIndex);
Expand All @@ -412,5 +526,41 @@ private void hitObjectHasSampleBank(int objectIndex, string bank) => AddAssert($
var h = EditorBeatmap.HitObjects.ElementAt(objectIndex);
return h.Samples.All(o => o.Bank == bank);
});

private void hitObjectHasSampleNormalBank(int objectIndex, string bank) => AddAssert($"{objectIndex.ToOrdinalWords()} has normal bank {bank}", () =>
{
var h = EditorBeatmap.HitObjects.ElementAt(objectIndex);
return h.Samples.Where(o => o.Name == HitSampleInfo.HIT_NORMAL).All(o => o.Bank == bank);
});

private void hitObjectHasSampleAdditionBank(int objectIndex, string bank) => AddAssert($"{objectIndex.ToOrdinalWords()} has addition bank {bank}", () =>
{
var h = EditorBeatmap.HitObjects.ElementAt(objectIndex);
return h.Samples.Where(o => o.Name != HitSampleInfo.HIT_NORMAL).All(o => o.Bank == bank);
});

private void hitObjectNodeHasSamples(int objectIndex, int nodeIndex, params string[] samples) => AddAssert($"{objectIndex.ToOrdinalWords()} object {nodeIndex.ToOrdinalWords()} node has samples {string.Join(',', samples)}", () =>
{
var h = EditorBeatmap.HitObjects.ElementAt(objectIndex) as IHasRepeats;
return h is not null && h.NodeSamples[nodeIndex].Select(s => s.Name).SequenceEqual(samples);
});

private void hitObjectNodeHasSampleBank(int objectIndex, int nodeIndex, string bank) => AddAssert($"{objectIndex.ToOrdinalWords()} object {nodeIndex.ToOrdinalWords()} node has bank {bank}", () =>
{
var h = EditorBeatmap.HitObjects.ElementAt(objectIndex) as IHasRepeats;
return h is not null && h.NodeSamples[nodeIndex].All(o => o.Bank == bank);
});

private void hitObjectNodeHasSampleNormalBank(int objectIndex, int nodeIndex, string bank) => AddAssert($"{objectIndex.ToOrdinalWords()} object {nodeIndex.ToOrdinalWords()} node has normal bank {bank}", () =>
{
var h = EditorBeatmap.HitObjects.ElementAt(objectIndex) as IHasRepeats;
return h is not null && h.NodeSamples[nodeIndex].Where(o => o.Name == HitSampleInfo.HIT_NORMAL).All(o => o.Bank == bank);
});

private void hitObjectNodeHasSampleAdditionBank(int objectIndex, int nodeIndex, string bank) => AddAssert($"{objectIndex.ToOrdinalWords()} object {nodeIndex.ToOrdinalWords()} node has addition bank {bank}", () =>
{
var h = EditorBeatmap.HitObjects.ElementAt(objectIndex) as IHasRepeats;
return h is not null && h.NodeSamples[nodeIndex].Where(o => o.Name != HitSampleInfo.HIT_NORMAL).All(o => o.Bank == bank);
});
}
}
12 changes: 10 additions & 2 deletions osu.Game/Rulesets/Objects/HitObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -224,8 +224,16 @@ public IList<HitSampleInfo> CreateSlidingSamples()
/// <returns>A populated <see cref="HitSampleInfo"/>.</returns>
public HitSampleInfo CreateHitSampleInfo(string sampleName = HitSampleInfo.HIT_NORMAL)
{
if (Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL) is HitSampleInfo existingSample)
return existingSample.With(newName: sampleName);
// As per stable, all non-normal "addition" samples should use the same bank.
if (sampleName != HitSampleInfo.HIT_NORMAL)
{
if (Samples.FirstOrDefault(s => s.Name != HitSampleInfo.HIT_NORMAL) is HitSampleInfo existingAddition)
return existingAddition.With(newName: sampleName);
}

// 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 new HitSampleInfo(sampleName);
}
Expand Down
15 changes: 15 additions & 0 deletions osu.Game/Rulesets/Objects/Types/IHasRepeats.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using osu.Game.Audio;
using System.Collections.Generic;
using System.Linq;

namespace osu.Game.Rulesets.Objects.Types
{
Expand Down Expand Up @@ -45,5 +46,19 @@ public static class HasRepeatsExtensions
public static IList<HitSampleInfo> GetNodeSamples<T>(this T obj, int nodeIndex)
where T : HitObject, IHasRepeats
=> nodeIndex < obj.NodeSamples.Count ? obj.NodeSamples[nodeIndex] : obj.Samples;

/// <summary>
/// Ensures that the list of node samples is at least as long as the number of nodes.
/// </summary>
/// <param name="obj">The <see cref="HitObject"/>.</param>
public static void PopulateNodeSamples<T>(this T obj)
where T : HitObject, IHasRepeats
{
if (obj.NodeSamples.Count >= obj.RepeatCount + 2)
return;

while (obj.NodeSamples.Count < obj.RepeatCount + 2)
obj.NodeSamples.Add(obj.Samples.Select(o => o.With()).ToList());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ protected virtual IEnumerable<TernaryButton> CreateTernaryButtons()
yield return new TernaryButton(NewCombo, "New combo", () => new SpriteIcon { Icon = OsuIcon.EditorNewComboA });

foreach (var kvp in SelectionHandler.SelectionSampleStates)
yield return new TernaryButton(kvp.Value, kvp.Key.Replace("hit", string.Empty).Titleize(), () => getIconForSample(kvp.Key));
yield return new TernaryButton(kvp.Value, kvp.Key.Replace("hit", string.Empty).Titleize(), () => GetIconForSample(kvp.Key));
}

private IEnumerable<TernaryButton> createSampleBankTernaryButtons()
Expand Down Expand Up @@ -264,7 +264,7 @@ private Drawable getIconForBank(string sampleName)
};
}

private Drawable getIconForSample(string sampleName)
public static Drawable GetIconForSample(string sampleName)
{
switch (sampleName)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ public void AddHitSample(string sampleName)
return;

h.Samples.Add(h.CreateHitSampleInfo(sampleName));

EditorBeatmap.Update(h);
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@ private void handleQuickDeletion(SelectionBlueprint<T> blueprint)
/// <summary>
/// Given a selection target and a function of truth, retrieve the correct ternary state for display.
/// </summary>
protected static TernaryState GetStateFromSelection<TObject>(IEnumerable<TObject> selection, Func<TObject, bool> func)
public static TernaryState GetStateFromSelection<TObject>(IEnumerable<TObject> selection, Func<TObject, bool> func)
{
if (selection.Any(func))
return selection.All(func) ? TernaryState.True : TernaryState.Indeterminate;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// 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 System.Collections.Generic;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Audio;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;

namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
public partial class NodeSamplePointPiece : SamplePointPiece
{
public readonly int NodeIndex;

public NodeSamplePointPiece(HitObject hitObject, int nodeIndex)
: base(hitObject)
{
if (hitObject is not IHasRepeats)
throw new System.ArgumentException($"HitObject must implement {nameof(IHasRepeats)}", nameof(hitObject));

NodeIndex = nodeIndex;
}

protected override IList<HitSampleInfo> GetSamples()
{
var hasRepeats = (IHasRepeats)HitObject;
return NodeIndex < hasRepeats.NodeSamples.Count ? hasRepeats.NodeSamples[NodeIndex] : HitObject.Samples;
}

public override Popover GetPopover() => new NodeSampleEditPopover(HitObject, NodeIndex);

public partial class NodeSampleEditPopover : SampleEditPopover
{
private readonly int nodeIndex;

protected override IList<HitSampleInfo> GetRelevantSamples(HitObject ho)
{
if (ho is not IHasRepeats hasRepeats)
return ho.Samples;

return nodeIndex < hasRepeats.NodeSamples.Count ? hasRepeats.NodeSamples[nodeIndex] : ho.Samples;
}

public NodeSampleEditPopover(HitObject hitObject, int nodeIndex)
: base(hitObject)
{
this.nodeIndex = nodeIndex;
}
}
}
}
Loading

0 comments on commit 316125d

Please sign in to comment.