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

Merge pull request #20829 from peppy/sample-set-controls

Add sample bank selection controls (and hotkeys) to editor
This commit is contained in:
Bartłomiej Dach 2023-05-25 22:49:46 +02:00 committed by GitHub
commit eb471091c7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 354 additions and 66 deletions

View File

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

View File

@ -91,7 +91,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
StartTime = 100,
Samples = new List<HitSampleInfo>
{
new HitSampleInfo(HitSampleInfo.HIT_NORMAL, "soft")
new HitSampleInfo(HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT)
}
};
hit.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
@ -100,13 +100,13 @@ namespace osu.Game.Rulesets.Taiko.Tests
});
AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit>);
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "soft");
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "soft");
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
AddStep("seek past hit", () => manualClock.CurrentTime = 200);
AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit>);
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "soft");
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "soft");
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
}
[Test]
@ -183,7 +183,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
EndTime = 1100,
Samples = new List<HitSampleInfo>
{
new HitSampleInfo(HitSampleInfo.HIT_NORMAL, "soft")
new HitSampleInfo(HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT)
}
};
drumRoll.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
@ -192,18 +192,18 @@ namespace osu.Game.Rulesets.Taiko.Tests
});
AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick>);
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "soft");
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "soft");
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
AddStep("seek to middle of drum roll", () => manualClock.CurrentTime = 600);
AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick>);
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "soft");
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "soft");
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
AddStep("seek past drum roll", () => manualClock.CurrentTime = 1200);
AddAssert("most valid object is drum roll", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRoll>);
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "soft");
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "soft");
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
}
[Test]

View File

@ -253,17 +253,17 @@ namespace osu.Game.Tests.Beatmaps.Formats
var soundPoint = controlPoints.SamplePointAt(0);
Assert.AreEqual(956, soundPoint.Time);
Assert.AreEqual("soft", soundPoint.SampleBank);
Assert.AreEqual(HitSampleInfo.BANK_SOFT, soundPoint.SampleBank);
Assert.AreEqual(60, soundPoint.SampleVolume);
soundPoint = controlPoints.SamplePointAt(53373);
Assert.AreEqual(53373, soundPoint.Time);
Assert.AreEqual("soft", soundPoint.SampleBank);
Assert.AreEqual(HitSampleInfo.BANK_SOFT, soundPoint.SampleBank);
Assert.AreEqual(60, soundPoint.SampleVolume);
soundPoint = controlPoints.SamplePointAt(119637);
Assert.AreEqual(119637, soundPoint.Time);
Assert.AreEqual("soft", soundPoint.SampleBank);
Assert.AreEqual(HitSampleInfo.BANK_SOFT, soundPoint.SampleBank);
Assert.AreEqual(80, soundPoint.SampleVolume);
var effectPoint = controlPoints.EffectPointAt(0);
@ -305,10 +305,10 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.That(controlPoints.EffectPointAt(2500).KiaiMode, Is.False);
Assert.That(controlPoints.EffectPointAt(3500).KiaiMode, Is.True);
Assert.That(controlPoints.SamplePointAt(500).SampleBank, Is.EqualTo("drum"));
Assert.That(controlPoints.SamplePointAt(1500).SampleBank, Is.EqualTo("drum"));
Assert.That(controlPoints.SamplePointAt(2500).SampleBank, Is.EqualTo("normal"));
Assert.That(controlPoints.SamplePointAt(3500).SampleBank, Is.EqualTo("drum"));
Assert.That(controlPoints.SamplePointAt(500).SampleBank, Is.EqualTo(HitSampleInfo.BANK_DRUM));
Assert.That(controlPoints.SamplePointAt(1500).SampleBank, Is.EqualTo(HitSampleInfo.BANK_DRUM));
Assert.That(controlPoints.SamplePointAt(2500).SampleBank, Is.EqualTo(HitSampleInfo.BANK_NORMAL));
Assert.That(controlPoints.SamplePointAt(3500).SampleBank, Is.EqualTo(HitSampleInfo.BANK_DRUM));
Assert.That(controlPoints.TimingPointAt(500).BeatLength, Is.EqualTo(500).Within(0.1));
Assert.That(controlPoints.TimingPointAt(1500).BeatLength, Is.EqualTo(500).Within(0.1));

View File

@ -13,6 +13,7 @@ using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
@ -52,7 +53,7 @@ namespace osu.Game.Tests.Visual.Editing
Position = (OsuPlayfield.BASE_SIZE + new Vector2(100, 0)) / 2,
Samples = new List<HitSampleInfo>
{
new HitSampleInfo(HitSampleInfo.HIT_NORMAL, "soft", volume: 60)
new HitSampleInfo(HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT, volume: 60)
}
});
});
@ -67,14 +68,14 @@ namespace osu.Game.Tests.Visual.Editing
hitObjectHasSampleBank(0, "normal");
hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_CLAP);
hitObjectHasSampleBank(1, "soft");
hitObjectHasSampleBank(1, HitSampleInfo.BANK_SOFT);
hitObjectHasSamples(1, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_CLAP);
AddStep("remove clap addition", () => InputManager.Key(Key.R));
hitObjectHasSampleBank(0, "normal");
hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL);
hitObjectHasSampleBank(1, "soft");
hitObjectHasSampleBank(1, HitSampleInfo.BANK_SOFT);
hitObjectHasSamples(1, HitSampleInfo.HIT_NORMAL);
}
@ -89,7 +90,7 @@ namespace osu.Game.Tests.Visual.Editing
public void TestSingleSelection()
{
clickSamplePiece(0);
samplePopoverHasSingleBank("normal");
samplePopoverHasSingleBank(HitSampleInfo.BANK_NORMAL);
samplePopoverHasSingleVolume(80);
dismissPopover();
@ -99,21 +100,21 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("select first object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.First()));
clickSamplePiece(1);
samplePopoverHasSingleBank("soft");
samplePopoverHasSingleBank(HitSampleInfo.BANK_SOFT);
samplePopoverHasSingleVolume(60);
setVolumeViaPopover(90);
hitObjectHasSampleVolume(1, 90);
setBankViaPopover("drum");
hitObjectHasSampleBank(1, "drum");
setBankViaPopover(HitSampleInfo.BANK_DRUM);
hitObjectHasSampleBank(1, HitSampleInfo.BANK_DRUM);
}
[Test]
public void TestUndo()
{
clickSamplePiece(1);
samplePopoverHasSingleBank("soft");
samplePopoverHasSingleBank(HitSampleInfo.BANK_SOFT);
samplePopoverHasSingleVolume(60);
setVolumeViaPopover(90);
@ -170,7 +171,7 @@ namespace osu.Game.Tests.Visual.Editing
}
[Test]
public void TestMultipleSelectionWithSameSampleBank()
public void TestPopoverMultipleSelectionWithSameSampleBank()
{
AddStep("unify sample bank", () =>
{
@ -178,33 +179,33 @@ namespace osu.Game.Tests.Visual.Editing
{
for (int i = 0; i < h.Samples.Count; i++)
{
h.Samples[i] = h.Samples[i].With(newBank: "soft");
h.Samples[i] = h.Samples[i].With(newBank: HitSampleInfo.BANK_SOFT);
}
}
});
AddStep("select both objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
clickSamplePiece(0);
samplePopoverHasSingleBank("soft");
samplePopoverHasSingleBank(HitSampleInfo.BANK_SOFT);
dismissPopover();
clickSamplePiece(1);
samplePopoverHasSingleBank("soft");
samplePopoverHasSingleBank(HitSampleInfo.BANK_SOFT);
setBankViaPopover(string.Empty);
hitObjectHasSampleBank(0, "soft");
hitObjectHasSampleBank(1, "soft");
samplePopoverHasSingleBank("soft");
hitObjectHasSampleBank(0, HitSampleInfo.BANK_SOFT);
hitObjectHasSampleBank(1, HitSampleInfo.BANK_SOFT);
samplePopoverHasSingleBank(HitSampleInfo.BANK_SOFT);
setBankViaPopover("drum");
hitObjectHasSampleBank(0, "drum");
hitObjectHasSampleBank(1, "drum");
samplePopoverHasSingleBank("drum");
setBankViaPopover(HitSampleInfo.BANK_DRUM);
hitObjectHasSampleBank(0, HitSampleInfo.BANK_DRUM);
hitObjectHasSampleBank(1, HitSampleInfo.BANK_DRUM);
samplePopoverHasSingleBank(HitSampleInfo.BANK_DRUM);
}
[Test]
public void TestMultipleSelectionWithDifferentSampleBank()
public void TestPopoverMultipleSelectionWithDifferentSampleBank()
{
AddStep("select both objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
clickSamplePiece(0);
@ -216,14 +217,109 @@ namespace osu.Game.Tests.Visual.Editing
samplePopoverHasIndeterminateBank();
setBankViaPopover(string.Empty);
hitObjectHasSampleBank(0, "normal");
hitObjectHasSampleBank(1, "soft");
hitObjectHasSampleBank(0, HitSampleInfo.BANK_NORMAL);
hitObjectHasSampleBank(1, HitSampleInfo.BANK_SOFT);
samplePopoverHasIndeterminateBank();
setBankViaPopover("normal");
hitObjectHasSampleBank(0, "normal");
hitObjectHasSampleBank(1, "normal");
samplePopoverHasSingleBank("normal");
setBankViaPopover(HitSampleInfo.BANK_NORMAL);
hitObjectHasSampleBank(0, HitSampleInfo.BANK_NORMAL);
hitObjectHasSampleBank(1, HitSampleInfo.BANK_NORMAL);
samplePopoverHasSingleBank(HitSampleInfo.BANK_NORMAL);
}
[Test]
public void TestHotkeysMultipleSelectionWithSameSampleBank()
{
AddStep("unify sample bank", () =>
{
foreach (var h in EditorBeatmap.HitObjects)
{
for (int i = 0; i < h.Samples.Count; i++)
{
h.Samples[i] = h.Samples[i].With(newBank: HitSampleInfo.BANK_SOFT);
}
}
});
AddStep("select both objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
hitObjectHasSampleBank(0, HitSampleInfo.BANK_SOFT);
hitObjectHasSampleBank(1, HitSampleInfo.BANK_SOFT);
AddStep("Press normal bank shortcut", () =>
{
InputManager.PressKey(Key.ShiftLeft);
InputManager.Key(Key.W);
InputManager.ReleaseKey(Key.ShiftLeft);
});
hitObjectHasSampleBank(0, HitSampleInfo.BANK_NORMAL);
hitObjectHasSampleBank(1, HitSampleInfo.BANK_NORMAL);
AddStep("Press drum bank shortcut", () =>
{
InputManager.PressKey(Key.ShiftLeft);
InputManager.Key(Key.R);
InputManager.ReleaseKey(Key.ShiftLeft);
});
hitObjectHasSampleBank(0, HitSampleInfo.BANK_DRUM);
hitObjectHasSampleBank(1, HitSampleInfo.BANK_DRUM);
AddStep("Press auto bank shortcut", () =>
{
InputManager.PressKey(Key.ShiftLeft);
InputManager.Key(Key.Q);
InputManager.ReleaseKey(Key.ShiftLeft);
});
// Should be a noop.
hitObjectHasSampleBank(0, HitSampleInfo.BANK_DRUM);
hitObjectHasSampleBank(1, HitSampleInfo.BANK_DRUM);
}
[Test]
public void TestHotkeysDuringPlacement()
{
AddStep("Enter placement mode", () => InputManager.Key(Key.Number2));
AddStep("Move mouse to centre", () => InputManager.MoveMouseTo(Editor.ChildrenOfType<HitObjectComposer>().First().ScreenSpaceDrawQuad.Centre));
AddStep("Move between two objects", () => EditorClock.Seek(250));
AddStep("Press normal bank shortcut", () =>
{
InputManager.PressKey(Key.ShiftLeft);
InputManager.Key(Key.W);
InputManager.ReleaseKey(Key.ShiftLeft);
});
checkPlacementSample(HitSampleInfo.BANK_NORMAL);
AddStep("Press drum bank shortcut", () =>
{
InputManager.PressKey(Key.ShiftLeft);
InputManager.Key(Key.R);
InputManager.ReleaseKey(Key.ShiftLeft);
});
checkPlacementSample(HitSampleInfo.BANK_DRUM);
AddStep("Press auto bank shortcut", () =>
{
InputManager.PressKey(Key.ShiftLeft);
InputManager.Key(Key.Q);
InputManager.ReleaseKey(Key.ShiftLeft);
});
checkPlacementSample(HitSampleInfo.BANK_NORMAL);
AddStep("Move after second object", () => EditorClock.Seek(750));
checkPlacementSample(HitSampleInfo.BANK_SOFT);
AddStep("Move to first object", () => EditorClock.Seek(0));
checkPlacementSample(HitSampleInfo.BANK_NORMAL);
void checkPlacementSample(string expected) => AddAssert($"Placement sample is {expected}", () => EditorBeatmap.PlacementObject.Value.Samples.First().Bank, () => Is.EqualTo(expected));
}
private void clickSamplePiece(int objectIndex) => AddStep($"click {objectIndex.ToOrdinalWords()} sample piece", () =>

View File

@ -73,7 +73,7 @@ namespace osu.Game.Tests.Visual.Gameplay
new HitCircle
{
StartTime = t += spacing,
Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, "soft") },
Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT) },
},
new HitCircle
{
@ -83,7 +83,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
StartTime = t += spacing,
Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, Vector2.UnitY * 200 }),
Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_WHISTLE, "soft") },
Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_WHISTLE, HitSampleInfo.BANK_SOFT) },
},
});

View File

@ -20,11 +20,20 @@ namespace osu.Game.Audio
public const string HIT_FINISH = @"hitfinish";
public const string HIT_CLAP = @"hitclap";
public const string BANK_NORMAL = @"normal";
public const string BANK_SOFT = @"soft";
public const string BANK_DRUM = @"drum";
/// <summary>
/// All valid sample addition constants.
/// </summary>
public static IEnumerable<string> AllAdditions => new[] { HIT_WHISTLE, HIT_FINISH, HIT_CLAP };
/// <summary>
/// All valid bank constants.
/// </summary>
public static IEnumerable<string> AllBanks => new[] { BANK_NORMAL, BANK_SOFT, BANK_DRUM };
/// <summary>
/// The name of the sample to load.
/// </summary>

View File

@ -14,7 +14,7 @@ namespace osu.Game.Beatmaps.ControlPoints
/// </remarks>
public class SampleControlPoint : ControlPoint, IEquatable<SampleControlPoint>
{
public const string DEFAULT_BANK = "normal";
public const string DEFAULT_BANK = HitSampleInfo.BANK_NORMAL;
public static readonly SampleControlPoint DEFAULT = new SampleControlPoint
{

View File

@ -12,6 +12,7 @@ using System.Linq;
using osu.Framework.Extensions;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Logging;
using osu.Game.Audio;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Legacy;
using osu.Game.Beatmaps.Timing;
@ -480,7 +481,7 @@ namespace osu.Game.Beatmaps.Formats
string stringSampleSet = sampleSet.ToString().ToLowerInvariant();
if (stringSampleSet == @"none")
stringSampleSet = @"normal";
stringSampleSet = HitSampleInfo.BANK_NORMAL;
if (timingChange)
{

View File

@ -589,13 +589,13 @@ namespace osu.Game.Beatmaps.Formats
{
switch (sampleBank?.ToLowerInvariant())
{
case "normal":
case HitSampleInfo.BANK_NORMAL:
return LegacySampleBank.Normal;
case "soft":
case HitSampleInfo.BANK_SOFT:
return LegacySampleBank.Soft;
case "drum":
case HitSampleInfo.BANK_DRUM:
return LegacySampleBank.Drum;
default:

View File

@ -77,6 +77,8 @@ namespace osu.Game.Rulesets.Edit
private FillFlowContainer togglesCollection;
private FillFlowContainer sampleBankTogglesCollection;
private IBindable<bool> hasTiming;
private Bindable<bool> autoSeekOnPlacement;
@ -160,6 +162,16 @@ namespace osu.Game.Rulesets.Edit
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 5),
},
},
new EditorToolboxGroup("bank (Shift-Q~R)")
{
Child = sampleBankTogglesCollection = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 5),
},
}
}
},
@ -197,6 +209,8 @@ namespace osu.Game.Rulesets.Edit
TernaryStates = CreateTernaryButtons().ToArray();
togglesCollection.AddRange(TernaryStates.Select(b => new DrawableTernaryButton(b)));
sampleBankTogglesCollection.AddRange(BlueprintContainer.SampleBankTernaryStates.Select(b => new DrawableTernaryButton(b)));
setSelectTool();
EditorBeatmap.SelectedHitObjects.CollectionChanged += selectionChanged;
@ -249,7 +263,7 @@ namespace osu.Game.Rulesets.Edit
/// <summary>
/// Create all ternary states required to be displayed to the user.
/// </summary>
protected virtual IEnumerable<TernaryButton> CreateTernaryButtons() => BlueprintContainer.TernaryStates;
protected virtual IEnumerable<TernaryButton> CreateTernaryButtons() => BlueprintContainer.MainTernaryStates;
/// <summary>
/// Construct a relevant blueprint container. This will manage hitobject selection/placement input handling and display logic.
@ -274,7 +288,7 @@ namespace osu.Game.Rulesets.Edit
protected override bool OnKeyDown(KeyDownEvent e)
{
if (e.ControlPressed || e.AltPressed || e.SuperPressed || e.ShiftPressed)
if (e.ControlPressed || e.AltPressed || e.SuperPressed)
return false;
if (checkLeftToggleFromKey(e.Key, out int leftIndex))
@ -291,7 +305,9 @@ namespace osu.Game.Rulesets.Edit
if (checkRightToggleFromKey(e.Key, out int rightIndex))
{
var item = togglesCollection.ElementAtOrDefault(rightIndex);
var item = e.ShiftPressed
? sampleBankTogglesCollection.ElementAtOrDefault(rightIndex)
: togglesCollection.ElementAtOrDefault(rightIndex);
if (item is DrawableTernaryButton button)
{

View File

@ -32,6 +32,11 @@ namespace osu.Game.Rulesets.Edit
/// </summary>
public PlacementState PlacementActive { get; private set; }
/// <summary>
/// Whether the sample bank should be taken from the previous hit object.
/// </summary>
public bool AutomaticBankAssignment { get; set; }
/// <summary>
/// The <see cref="HitObject"/> that is being placed.
/// </summary>
@ -86,11 +91,6 @@ namespace osu.Game.Rulesets.Edit
/// <param name="commitStart">Whether this call is committing a value for HitObject.StartTime and continuing with further adjustments.</param>
protected void BeginPlacement(bool commitStart = false)
{
// Take the hitnormal sample of the last hit object
var lastHitNormal = getPreviousHitObject()?.Samples?.FirstOrDefault(o => o.Name == HitSampleInfo.HIT_NORMAL);
if (lastHitNormal != null)
HitObject.Samples[0] = lastHitNormal;
placementHandler.BeginPlacement(HitObject);
if (commitStart)
PlacementActive = PlacementState.Active;
@ -155,6 +155,14 @@ namespace osu.Game.Rulesets.Edit
if (HitObject is IHasComboInformation comboInformation)
comboInformation.UpdateComboInformation(getPreviousHitObject() as IHasComboInformation);
}
if (AutomaticBankAssignment)
{
// Take the hitnormal sample of the last hit object
var lastHitNormal = getPreviousHitObject()?.Samples?.FirstOrDefault(o => o.Name == HitSampleInfo.HIT_NORMAL);
if (lastHitNormal != null)
HitObject.Samples[0] = lastHitNormal;
}
}
/// <summary>

View File

@ -14,6 +14,8 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Game.Audio;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
@ -56,7 +58,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
[BackgroundDependencyLoader]
private void load()
{
TernaryStates = CreateTernaryButtons().ToArray();
MainTernaryStates = CreateTernaryButtons().ToArray();
SampleBankTernaryStates = createSampleBankTernaryButtons().ToArray();
AddInternal(new DrawableRulesetDependenciesProvidingContainer(Composer.Ruleset)
{
@ -78,9 +81,10 @@ namespace osu.Game.Screens.Edit.Compose.Components
// we own SelectionHandler so don't need to worry about making bindable copies (for simplicity)
foreach (var kvp in SelectionHandler.SelectionSampleStates)
{
kvp.Value.BindValueChanged(_ => updatePlacementSamples());
}
foreach (var kvp in SelectionHandler.SelectionBankStates)
kvp.Value.BindValueChanged(_ => updatePlacementSamples());
}
protected override void TransferBlueprintFor(HitObject hitObject, DrawableHitObject drawableObject)
@ -166,6 +170,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
foreach (var kvp in SelectionHandler.SelectionSampleStates)
sampleChanged(kvp.Key, kvp.Value.Value);
foreach (var kvp in SelectionHandler.SelectionBankStates)
bankChanged(kvp.Key, kvp.Value.Value);
}
private void sampleChanged(string sampleName, TernaryState state)
@ -190,12 +197,24 @@ namespace osu.Game.Screens.Edit.Compose.Components
}
}
private void bankChanged(string bankName, TernaryState state)
{
if (CurrentPlacement == null) return;
if (bankName == EditorSelectionHandler.HIT_BANK_AUTO)
CurrentPlacement.AutomaticBankAssignment = state == TernaryState.True;
else if (state == TernaryState.True)
CurrentPlacement.HitObject.Samples = CurrentPlacement.HitObject.Samples.Select(s => s.With(newBank: bankName)).ToList();
}
public readonly Bindable<TernaryState> NewCombo = new Bindable<TernaryState> { Description = "New Combo" };
/// <summary>
/// A collection of states which will be displayed to the user in the toolbox.
/// </summary>
public TernaryButton[] TernaryStates { get; private set; }
public TernaryButton[] MainTernaryStates { get; private set; }
public TernaryButton[] SampleBankTernaryStates { get; private set; }
/// <summary>
/// Create all ternary states required to be displayed to the user.
@ -209,6 +228,39 @@ namespace osu.Game.Screens.Edit.Compose.Components
yield return new TernaryButton(kvp.Value, kvp.Key.Replace("hit", string.Empty).Titleize(), () => getIconForSample(kvp.Key));
}
private IEnumerable<TernaryButton> createSampleBankTernaryButtons()
{
foreach (var kvp in SelectionHandler.SelectionBankStates)
yield return new TernaryButton(kvp.Value, kvp.Key.Titleize(), () => getIconForBank(kvp.Key));
}
private Drawable getIconForBank(string sampleName)
{
return new Container
{
Size = new Vector2(30, 20),
Children = new Drawable[]
{
new SpriteIcon
{
Size = new Vector2(8),
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Icon = FontAwesome.Solid.VolumeOff
},
new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
X = 10,
Y = -1,
Font = OsuFont.Default.With(weight: FontWeight.Bold, size: 20),
Text = $"{char.ToUpperInvariant(sampleName.First())}"
}
}
};
}
private Drawable getIconForSample(string sampleName)
{
switch (sampleName)

View File

@ -11,6 +11,7 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Audio;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
@ -20,6 +21,12 @@ namespace osu.Game.Screens.Edit.Compose.Components
{
public partial class EditorSelectionHandler : SelectionHandler<HitObject>
{
/// <summary>
/// A special bank name that is only used in the editor UI.
/// When selected and in placement mode, the bank of the last hit object will always be used.
/// </summary>
public const string HIT_BANK_AUTO = "auto";
[Resolved]
protected EditorBeatmap EditorBeatmap { get; private set; }
@ -48,11 +55,83 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// </summary>
public readonly Dictionary<string, Bindable<TernaryState>> SelectionSampleStates = new Dictionary<string, Bindable<TernaryState>>();
/// <summary>
/// The state of each sample bank type for all selected hitobjects.
/// </summary>
public readonly Dictionary<string, Bindable<TernaryState>> SelectionBankStates = new Dictionary<string, Bindable<TernaryState>>();
/// <summary>
/// Set up ternary state bindables and bind them to selection/hitobject changes (in both directions)
/// </summary>
private void createStateBindables()
{
foreach (string bankName in HitSampleInfo.AllBanks.Prepend(HIT_BANK_AUTO))
{
var bindable = new Bindable<TernaryState>
{
Description = bankName.Titleize()
};
bindable.ValueChanged += state =>
{
switch (state.NewValue)
{
case TernaryState.False:
if (SelectedItems.Count == 0)
{
// Ensure that if this is the last selected bank, it should remain selected.
if (SelectionBankStates.Values.All(b => b.Value == TernaryState.False))
bindable.Value = TernaryState.True;
}
else
{
// Auto should never apply when there is a selection made.
// This is also required to stop a bindable feedback loop when a HitObject has zero samples (and LINQ `All` below becomes true).
if (bankName == HIT_BANK_AUTO)
break;
// Never remove a sample bank.
// These are basically radio buttons, not toggles.
if (SelectedItems.All(h => h.Samples.All(s => s.Bank == bankName)))
bindable.Value = TernaryState.True;
}
break;
case TernaryState.True:
if (SelectedItems.Count == 0)
{
// Ensure the user can't stack multiple bank selections when there's no hitobject selection.
// Note that in normal scenarios this is sorted out by the feedback from applying the bank to the selected objects.
foreach (var other in SelectionBankStates.Values)
{
if (other != bindable)
other.Value = TernaryState.False;
}
}
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;
}
AddSampleBank(bankName);
}
break;
}
};
SelectionBankStates[bankName] = bindable;
}
// start with normal selected.
SelectionBankStates[SampleControlPoint.DEFAULT_BANK].Value = TernaryState.True;
foreach (string sampleName in HitSampleInfo.AllAdditions)
{
var bindable = new Bindable<TernaryState>
@ -104,12 +183,33 @@ namespace osu.Game.Screens.Edit.Compose.Components
{
bindable.Value = GetStateFromSelection(SelectedItems, h => h.Samples.Any(s => s.Name == sampleName));
}
foreach ((string bankName, var bindable) in SelectionBankStates)
{
bindable.Value = GetStateFromSelection(SelectedItems, h => h.Samples.All(s => s.Bank == bankName));
}
}
#endregion
#region Ternary state changes
/// <summary>
/// Adds a sample bank to all selected <see cref="HitObject"/>s.
/// </summary>
/// <param name="bankName">The name of the sample bank.</param>
public void AddSampleBank(string bankName)
{
EditorBeatmap.PerformOnSelection(h =>
{
if (h.Samples.All(s => s.Bank == bankName))
return;
h.Samples = h.Samples.Select(s => s.With(newBank: bankName)).ToList();
EditorBeatmap.Update(h);
});
}
/// <summary>
/// Adds a hit sample to all selected <see cref="HitObject"/>s.
/// </summary>
@ -174,11 +274,17 @@ namespace osu.Game.Screens.Edit.Compose.Components
yield return new TernaryStateToggleMenuItem("New combo") { State = { BindTarget = SelectionNewComboState } };
}
yield return new OsuMenuItem("Sound")
yield return new OsuMenuItem("Sample")
{
Items = SelectionSampleStates.Select(kvp =>
new TernaryStateToggleMenuItem(kvp.Value.Description) { State = { BindTarget = kvp.Value } }).ToArray()
};
yield return new OsuMenuItem("Bank")
{
Items = SelectionBankStates.Select(kvp =>
new TernaryStateToggleMenuItem(kvp.Value.Description) { State = { BindTarget = kvp.Value } }).ToArray()
};
}
#endregion