Skip to content

Commit

Permalink
Merge pull request #31473 from bdach/colorhax
Browse files Browse the repository at this point in the history
Add combo colour override control to editor
  • Loading branch information
peppy authored Jan 14, 2025
2 parents f2b7984 + 8211c4e commit 0e20c0e
Show file tree
Hide file tree
Showing 6 changed files with 289 additions and 13 deletions.
3 changes: 1 addition & 2 deletions osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Edit.Components.TernaryButtons;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK;

Expand Down Expand Up @@ -72,7 +71,7 @@ private void load()

protected override Drawable CreateHitObjectInspector() => new CatchHitObjectInspector(DistanceSnapProvider);

protected override IEnumerable<DrawableTernaryButton> CreateTernaryButtons()
protected override IEnumerable<Drawable> CreateTernaryButtons()
=> base.CreateTernaryButtons()
.Concat(DistanceSnapProvider.CreateTernaryButtons());

Expand Down
2 changes: 1 addition & 1 deletion osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ protected override DrawableRuleset<OsuHitObject> CreateDrawableRuleset(Ruleset r

protected override Drawable CreateHitObjectInspector() => new OsuHitObjectInspector();

protected override IEnumerable<DrawableTernaryButton> CreateTernaryButtons()
protected override IEnumerable<Drawable> CreateTernaryButtons()
=> base.CreateTernaryButtons()
.Append(new DrawableTernaryButton
{
Expand Down
5 changes: 3 additions & 2 deletions osu.Game/Rulesets/Edit/HitObjectComposer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Framework.Logging;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics;
Expand Down Expand Up @@ -370,7 +371,7 @@ protected override void Update()
/// <summary>
/// Create all ternary states required to be displayed to the user.
/// </summary>
protected virtual IEnumerable<DrawableTernaryButton> CreateTernaryButtons() => BlueprintContainer.MainTernaryStates;
protected virtual IEnumerable<Drawable> CreateTernaryButtons() => BlueprintContainer.MainTernaryStates;

/// <summary>
/// Construct a relevant blueprint container. This will manage hitobject selection/placement input handling and display logic.
Expand Down Expand Up @@ -429,7 +430,7 @@ protected override bool OnKeyDown(KeyDownEvent e)
}
else
{
if (togglesCollection.ElementAtOrDefault(rightIndex) is DrawableTernaryButton button)
if (togglesCollection.ChildrenOfType<DrawableTernaryButton>().ElementAtOrDefault(rightIndex) is DrawableTernaryButton button)
{
button.Toggle();
return true;
Expand Down
3 changes: 3 additions & 0 deletions osu.Game/Rulesets/Objects/Types/IHasComboInformation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ public interface IHasComboInformation : IHasCombo
/// </summary>
new bool NewCombo { get; set; }

/// <inheritdoc cref="IHasCombo.ComboOffset"/>
new int ComboOffset { get; set; }

/// <summary>
/// Bindable exposure of <see cref="LastInCombo"/>.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
// 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;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Overlays;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osuTK;

namespace osu.Game.Screens.Edit.Components.TernaryButtons
{
public partial class NewComboTernaryButton : CompositeDrawable, IHasCurrentValue<TernaryState>
{
public Bindable<TernaryState> Current
{
get => current.Current;
set => current.Current = value;
}

private readonly BindableWithCurrent<TernaryState> current = new BindableWithCurrent<TernaryState>();

private readonly BindableList<HitObject> selectedHitObjects = new BindableList<HitObject>();
private readonly BindableList<Colour4> comboColours = new BindableList<Colour4>();

private Container mainButtonContainer = null!;
private ColourPickerButton pickerButton = null!;

[BackgroundDependencyLoader]
private void load(EditorBeatmap editorBeatmap)
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
InternalChildren = new Drawable[]
{
mainButtonContainer = new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Child = new DrawableTernaryButton
{
Current = Current,
Description = "New combo",
CreateIcon = () => new SpriteIcon { Icon = OsuIcon.EditorNewComboA },
},
},
pickerButton = new ColourPickerButton
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Alpha = 0,
Width = 25,
ComboColours = { BindTarget = comboColours }
}
};

selectedHitObjects.BindTo(editorBeatmap.SelectedHitObjects);
if (editorBeatmap.BeatmapSkin != null)
comboColours.BindTo(editorBeatmap.BeatmapSkin.ComboColours);
}

protected override void LoadComplete()
{
base.LoadComplete();

selectedHitObjects.BindCollectionChanged((_, _) => updateState());
comboColours.BindCollectionChanged((_, _) => updateState());
Current.BindValueChanged(_ => updateState(), true);
}

private void updateState()
{
if (Current.Value == TernaryState.True && selectedHitObjects.Count == 1 && selectedHitObjects.Single() is IHasComboInformation hasCombo && comboColours.Count > 1)
{
mainButtonContainer.Padding = new MarginPadding { Right = 30 };
pickerButton.SelectedHitObject.Value = hasCombo;
pickerButton.Alpha = 1;
}
else
{
mainButtonContainer.Padding = new MarginPadding();
pickerButton.Alpha = 0;
}
}

private partial class ColourPickerButton : OsuButton, IHasPopover
{
public BindableList<Colour4> ComboColours { get; } = new BindableList<Colour4>();
public Bindable<IHasComboInformation?> SelectedHitObject { get; } = new Bindable<IHasComboInformation?>();

[Resolved]
private EditorBeatmap editorBeatmap { get; set; } = null!;

[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;

private SpriteIcon icon = null!;

[BackgroundDependencyLoader]
private void load()
{
Add(icon = new SpriteIcon
{
Icon = FontAwesome.Solid.Palette,
Size = new Vector2(16),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
});

Action = this.ShowPopover;
}

protected override void LoadComplete()
{
base.LoadComplete();
ComboColours.BindCollectionChanged((_, _) => updateState());
SelectedHitObject.BindValueChanged(val =>
{
if (val.OldValue != null)
val.OldValue.ComboIndexWithOffsetsBindable.ValueChanged -= onComboIndexChanged;

updateState();

if (val.NewValue != null)
val.NewValue.ComboIndexWithOffsetsBindable.ValueChanged += onComboIndexChanged;
}, true);
}

private void onComboIndexChanged(ValueChangedEvent<int> _) => updateState();

private void updateState()
{
Enabled.Value = SelectedHitObject.Value != null;

if (SelectedHitObject.Value == null || SelectedHitObject.Value.ComboOffset == 0)
{
BackgroundColour = colourProvider.Background3;
icon.Colour = BackgroundColour.Darken(0.5f);
icon.Blending = BlendingParameters.Additive;
}
else
{
BackgroundColour = ComboColours[comboIndexFor(SelectedHitObject.Value, ComboColours)];
icon.Colour = OsuColour.ForegroundTextColourFor(BackgroundColour);
icon.Blending = BlendingParameters.Inherit;
}
}

public Popover GetPopover() => new ComboColourPalettePopover(ComboColours, SelectedHitObject.Value.AsNonNull(), editorBeatmap);
}

private partial class ComboColourPalettePopover : OsuPopover
{
private readonly IReadOnlyList<Colour4> comboColours;
private readonly IHasComboInformation hasComboInformation;
private readonly EditorBeatmap editorBeatmap;

public ComboColourPalettePopover(IReadOnlyList<Colour4> comboColours, IHasComboInformation hasComboInformation, EditorBeatmap editorBeatmap)
{
this.comboColours = comboColours;
this.hasComboInformation = hasComboInformation;
this.editorBeatmap = editorBeatmap;

AllowableAnchors = [Anchor.CentreRight];
}

[BackgroundDependencyLoader]
private void load()
{
Debug.Assert(comboColours.Count > 0);
var hitObject = hasComboInformation as HitObject;
Debug.Assert(hitObject != null);

FillFlowContainer container;

Child = container = new FillFlowContainer
{
Width = 230,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(10),
};

int selectedColourIndex = comboIndexFor(hasComboInformation, comboColours);

for (int i = 0; i < comboColours.Count; i++)
{
int index = i;

if (getPreviousHitObjectWithCombo(editorBeatmap, hitObject) is IHasComboInformation previousHasCombo
&& index == comboIndexFor(previousHasCombo, comboColours)
&& !canReuseLastComboColour(editorBeatmap, hitObject))
{
continue;
}

container.Add(new OsuClickableContainer
{
Size = new Vector2(50),
Masking = true,
CornerRadius = 25,
Children = new[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = comboColours[index],
},
selectedColourIndex == index
? new SpriteIcon
{
Icon = FontAwesome.Solid.Check,
Size = new Vector2(24),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Colour = OsuColour.ForegroundTextColourFor(comboColours[index]),
}
: Empty()
},
Action = () =>
{
int comboDifference = index - selectedColourIndex;
if (comboDifference == 0)
return;

int newOffset = hasComboInformation.ComboOffset + comboDifference;
// `newOffset` must be positive to serialise correctly - this implements the true math "modulus" rather than the built-in "remainder" % op
// which can return negative results when the first operand is negative
newOffset -= (int)Math.Floor((double)newOffset / comboColours.Count) * comboColours.Count;

hasComboInformation.ComboOffset = newOffset;
editorBeatmap.BeginChange();
editorBeatmap.Update((HitObject)hasComboInformation);
editorBeatmap.EndChange();
this.HidePopover();
}
});
}
}

private static IHasComboInformation? getPreviousHitObjectWithCombo(EditorBeatmap editorBeatmap, HitObject hitObject)
=> editorBeatmap.HitObjects.TakeWhile(ho => ho != hitObject).LastOrDefault() as IHasComboInformation;

private static bool canReuseLastComboColour(EditorBeatmap editorBeatmap, HitObject hitObject)
{
double? closestBreakEnd = editorBeatmap.Breaks.Select(b => b.EndTime)
.Where(t => t <= hitObject.StartTime)
.OrderBy(t => t)
.LastOrDefault();

if (closestBreakEnd == null)
return false;

return editorBeatmap.HitObjects.FirstOrDefault(ho => ho.StartTime >= closestBreakEnd) == hitObject;
}
}

// compare `EditorBeatmapSkin.updateColours()` et al. for reasoning behind the off-by-one index rotation
private static int comboIndexFor(IHasComboInformation hasComboInformation, IReadOnlyCollection<Colour4> comboColours)
=> (hasComboInformation.ComboIndexWithOffsets + comboColours.Count - 1) % comboColours.Count;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -237,22 +237,17 @@ private void additionBankChanged(string bankName, TernaryState state)
/// <summary>
/// A collection of states which will be displayed to the user in the toolbox.
/// </summary>
public DrawableTernaryButton[] MainTernaryStates { get; private set; }
public Drawable[] MainTernaryStates { get; private set; }

public SampleBankTernaryButton[] SampleBankTernaryStates { get; private set; }

/// <summary>
/// Create all ternary states required to be displayed to the user.
/// </summary>
protected virtual IEnumerable<DrawableTernaryButton> CreateTernaryButtons()
protected virtual IEnumerable<Drawable> CreateTernaryButtons()
{
//TODO: this should only be enabled (visible?) for rulesets that provide combo-supporting HitObjects.
yield return new DrawableTernaryButton
{
Current = NewCombo,
Description = "New combo",
CreateIcon = () => new SpriteIcon { Icon = OsuIcon.EditorNewComboA },
};
yield return new NewComboTernaryButton { Current = NewCombo };

foreach (var kvp in SelectionHandler.SelectionSampleStates)
{
Expand Down

0 comments on commit 0e20c0e

Please sign in to comment.