1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-17 19:04:00 +08:00
Files
osu-lazer/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs
T
Bartłomiej Dach 7e2771c3f0 Improve usability of sample bank toggles (#36753)
- [x] Depends on https://github.com/ppy/osu/pull/36741 for merge
conflict avoidance

RFC, cc @OliBomby

## [Adjust behaviour of automatic bank assignment during
placement](https://github.com/ppy/osu/commit/547f55e9b3ded668fe6e1c8865a2d625e64a2f45)

Diatribe time!

This is fallout of the discussion about auto bank in
https://github.com/ppy/osu/issues/36705.

Auto bank in lazer as written before this commit is confused. On stable,
auto bank is closer to "no bank", as in "go look up the current sample
timing point, get the bank of that, and use that". lazer has no timing
points anymore, but people still want auto bank. So what do?

Auto bank for normal samples is somewhat sane still. It only works
during placement, and will just copy the normal bank of the previous
object - if one exists. That said, one *might not* exist, but the
resulting object will still have its normal sample created with
`editorAutoBank: true`. That is largely cosmetic and without
consequences, but this commit fixes that.

Auto bank for *addition* samples, however... Hoo boy.

- For placed objects, auto bank means "take the normal sample, read its
bank, and use that". Simple enough, right?
- Hoooooowever. During placement, auto bank before this commit used to
mean "look at the *previous object*, check if it has an addition sound
and then use its bank, if not use *the previous object's* normal sample
and then use its bank" which is a completely different thing with its
own implications. Like, say, what happens if the previous object uses
the auto addition bank too? What should be copied over? Should it be the
notion of "auto bank" in that the addition bank should match the normal
bank, or should it be the literal bank that the previous object is
using?

This change attempts to define this unambiguously. "Auto additions bank"
means "the same bank as the normal bank of this object", full stop.

## [Do not touch sample toggle state if there are no selected
objects](https://github.com/ppy/osu/commit/052cde5987e48800ec68ab2528c7e0ce3140e6e0)

Fixes issue described in
https://github.com/ppy/osu/issues/36705#issuecomment-3953917163 wherein
opening a sample popover will disable addition bank toggles and toggle
off all addition samples.

---------

Co-authored-by: Dean Herbert <pe@ppy.sh>
2026-02-26 15:18:29 +09:00

170 lines
6.5 KiB
C#

// 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.Linq;
using System.Threading;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Compose;
using osuTK;
namespace osu.Game.Rulesets.Edit
{
/// <summary>
/// A blueprint which governs the creation of a new <see cref="HitObject"/> to actualisation.
/// </summary>
public abstract partial class HitObjectPlacementBlueprint : PlacementBlueprint
{
/// <summary>
/// Whether the sample bank should be taken from the previous hit object.
/// </summary>
public bool AutomaticBankAssignment { get; set; }
/// <summary>
/// Whether the sample addition bank should be taken from the previous hit objects.
/// </summary>
public bool AutomaticAdditionBankAssignment { get; set; }
/// <summary>
/// The <see cref="HitObject"/> that is being placed.
/// </summary>
public readonly HitObject HitObject;
[Resolved]
protected EditorClock EditorClock { get; private set; } = null!;
[Resolved]
private EditorBeatmap beatmap { get; set; } = null!;
private Bindable<double> startTimeBindable = null!;
private HitObject? getPreviousHitObject() => beatmap.HitObjects.TakeWhile(h => h.StartTime <= startTimeBindable.Value).LastOrDefault();
protected override bool IsValidForPlacement => HitObject.StartTime >= beatmap.ControlPointInfo.TimingPoints.FirstOrDefault()?.Time;
[Resolved]
private IPlacementHandler placementHandler { get; set; } = null!;
protected HitObjectPlacementBlueprint(HitObject hitObject)
{
HitObject = hitObject;
// adding the default hit sample should be the case regardless of the ruleset.
HitObject.Samples.Add(new HitSampleInfo(HitSampleInfo.HIT_NORMAL));
}
[BackgroundDependencyLoader]
private void load()
{
startTimeBindable = HitObject.StartTimeBindable.GetBoundCopy();
startTimeBindable.BindValueChanged(_ => ApplyDefaultsToHitObject(), true);
}
private bool placementBegun;
protected override void BeginPlacement(bool commitStart = false)
{
base.BeginPlacement(commitStart);
if (State.Value == Visibility.Visible)
placementHandler.ShowPlacement(HitObject);
placementBegun = true;
}
public override void EndPlacement(bool commit)
{
base.EndPlacement(commit);
if (IsValidForPlacement && commit)
placementHandler.CommitPlacement(HitObject);
else
placementHandler.HidePlacement();
}
protected override void Update()
{
base.Update();
Colour = IsValidForPlacement ? Colour4.White : Colour4.Red;
}
/// <summary>
/// Updates the time and position of this <see cref="PlacementBlueprint"/>.
/// </summary>
public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double time)
{
if (PlacementActive == PlacementState.Waiting)
{
HitObject.StartTime = time;
if (HitObject is IHasComboInformation comboInformation)
comboInformation.UpdateComboInformation(getPreviousHitObject() as IHasComboInformation);
}
var lastHitObject = getPreviousHitObject();
var lastHitNormal = lastHitObject?.Samples?.FirstOrDefault(o => o.Name == HitSampleInfo.HIT_NORMAL);
if (lastHitNormal != null && AutomaticBankAssignment)
// Inherit the bank from the previous hit object
HitObject.Samples = HitObject.Samples.Select(s => s.Name == HitSampleInfo.HIT_NORMAL ? s.With(newBank: lastHitNormal.Bank, newEditorAutoBank: true) : s).ToList();
else
HitObject.Samples = HitObject.Samples.Select(s => s.Name == HitSampleInfo.HIT_NORMAL ? s.With(newEditorAutoBank: false) : s).ToList();
if (lastHitNormal != null)
{
// Inherit the volume and sample set info from the previous hit object
HitObject.Samples = HitObject.Samples.Select(s => s.With(
newVolume: lastHitNormal.Volume,
newSuffix: lastHitNormal.Suffix,
newUseBeatmapSamples: lastHitNormal.UseBeatmapSamples)).ToList();
}
if (AutomaticAdditionBankAssignment)
{
string bank = HitObject.Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL)?.Bank ?? HitSampleInfo.BANK_SOFT;
HitObject.Samples = HitObject.Samples.Select(s => s.Name != HitSampleInfo.HIT_NORMAL ? s.With(newBank: bank, newEditorAutoBank: true) : s).ToList();
}
else
HitObject.Samples = HitObject.Samples.Select(s => s.Name != HitSampleInfo.HIT_NORMAL ? s.With(newEditorAutoBank: false) : s).ToList();
if (HitObject is IHasRepeats hasRepeats)
{
// Make sure all the node samples are identical to the hit object's samples
for (int i = 0; i < hasRepeats.NodeSamples.Count; i++)
hasRepeats.NodeSamples[i] = HitObject.Samples.Select(o => o.With()).ToList();
}
return new SnapResult(screenSpacePosition, time);
}
/// <summary>
/// Invokes <see cref="Objects.HitObject.ApplyDefaults(ControlPointInfo,IBeatmapDifficultyInfo,CancellationToken)"/>,
/// refreshing <see cref="Objects.HitObject.NestedHitObjects"/> and parameters for the <see cref="HitObject"/>.
/// </summary>
protected void ApplyDefaultsToHitObject() => HitObject.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty);
protected override void PopIn()
{
base.PopIn();
if (placementBegun)
placementHandler.ShowPlacement(HitObject);
}
protected override void PopOut()
{
base.PopOut();
placementHandler.HidePlacement();
}
}
}