Merge pull request #17226 from peppy/skin-component-settings

Allow skin components to have settings
This commit is contained in:
Dan Balasescu
2022-03-16 17:12:11 +09:00
committed by GitHub
13 changed files with 240 additions and 51 deletions

View File

@ -6,6 +6,7 @@
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using JetBrains.Annotations; using JetBrains.Annotations;
@ -170,6 +171,39 @@ namespace osu.Game.Configuration
private static readonly ConcurrentDictionary<Type, (SettingSourceAttribute, PropertyInfo)[]> property_info_cache = new ConcurrentDictionary<Type, (SettingSourceAttribute, PropertyInfo)[]>(); private static readonly ConcurrentDictionary<Type, (SettingSourceAttribute, PropertyInfo)[]> property_info_cache = new ConcurrentDictionary<Type, (SettingSourceAttribute, PropertyInfo)[]>();
/// <summary>
/// Returns the underlying value of the given mod setting object.
/// Can be used for serialization and equality comparison purposes.
/// </summary>
/// <param name="setting">A <see cref="SettingSourceAttribute"/> bindable.</param>
public static object GetUnderlyingSettingValue(this object setting)
{
switch (setting)
{
case Bindable<double> d:
return d.Value;
case Bindable<int> i:
return i.Value;
case Bindable<float> f:
return f.Value;
case Bindable<bool> b:
return b.Value;
case IBindable u:
// An unknown (e.g. enum) generic type.
var valueMethod = u.GetType().GetProperty(nameof(IBindable<int>.Value));
Debug.Assert(valueMethod != null);
return valueMethod.GetValue(u);
default:
// fall back for non-bindable cases.
return setting;
}
}
public static IEnumerable<(SettingSourceAttribute, PropertyInfo)> GetSettingsSourceProperties(this object obj) public static IEnumerable<(SettingSourceAttribute, PropertyInfo)> GetSettingsSourceProperties(this object obj)
{ {
var type = obj.GetType(); var type = obj.GetType();

View File

@ -1,8 +1,11 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using Humanizer;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Configuration;
using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK; using osuTK;
@ -59,8 +62,18 @@ namespace osu.Game.Extensions
component.Origin = info.Origin; component.Origin = info.Origin;
if (component is ISkinnableDrawable skinnable) if (component is ISkinnableDrawable skinnable)
{
skinnable.UsesFixedAnchor = info.UsesFixedAnchor; skinnable.UsesFixedAnchor = info.UsesFixedAnchor;
foreach (var (_, property) in component.GetSettingsSourceProperties())
{
if (!info.Settings.TryGetValue(property.Name.Underscore(), out object settingValue))
continue;
skinnable.CopyAdjustedSetting((IBindable)property.GetValue(component), settingValue);
}
}
if (component is Container container) if (component is Container container)
{ {
foreach (var child in info.Children) foreach (var child in info.Children)

View File

@ -12,7 +12,6 @@ using osu.Framework.Logging;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Utils;
namespace osu.Game.Online.API namespace osu.Game.Online.API
{ {
@ -43,7 +42,7 @@ namespace osu.Game.Online.API
var bindable = (IBindable)property.GetValue(mod); var bindable = (IBindable)property.GetValue(mod);
if (!bindable.IsDefault) if (!bindable.IsDefault)
Settings.Add(property.Name.Underscore(), ModUtils.GetSettingUnderlyingValue(bindable)); Settings.Add(property.Name.Underscore(), bindable.GetUnderlyingSettingValue());
} }
} }
@ -93,13 +92,13 @@ namespace osu.Game.Online.API
public bool Equals(KeyValuePair<string, object> x, KeyValuePair<string, object> y) public bool Equals(KeyValuePair<string, object> x, KeyValuePair<string, object> y)
{ {
object xValue = ModUtils.GetSettingUnderlyingValue(x.Value); object xValue = x.Value.GetUnderlyingSettingValue();
object yValue = ModUtils.GetSettingUnderlyingValue(y.Value); object yValue = y.Value.GetUnderlyingSettingValue();
return x.Key == y.Key && EqualityComparer<object>.Default.Equals(xValue, yValue); return x.Key == y.Key && EqualityComparer<object>.Default.Equals(xValue, yValue);
} }
public int GetHashCode(KeyValuePair<string, object> obj) => HashCode.Combine(obj.Key, ModUtils.GetSettingUnderlyingValue(obj.Value)); public int GetHashCode(KeyValuePair<string, object> obj) => HashCode.Combine(obj.Key, obj.Value.GetUnderlyingSettingValue());
} }
} }
} }

View File

@ -6,7 +6,7 @@ using System.Collections.Generic;
using System.Text; using System.Text;
using MessagePack; using MessagePack;
using MessagePack.Formatters; using MessagePack.Formatters;
using osu.Game.Utils; using osu.Game.Configuration;
namespace osu.Game.Online.API namespace osu.Game.Online.API
{ {
@ -23,7 +23,7 @@ namespace osu.Game.Online.API
var stringBytes = new ReadOnlySequence<byte>(Encoding.UTF8.GetBytes(kvp.Key)); var stringBytes = new ReadOnlySequence<byte>(Encoding.UTF8.GetBytes(kvp.Key));
writer.WriteString(in stringBytes); writer.WriteString(in stringBytes);
primitiveFormatter.Serialize(ref writer, ModUtils.GetSettingUnderlyingValue(kvp.Value), options); primitiveFormatter.Serialize(ref writer, kvp.Value.GetUnderlyingSettingValue(), options);
} }
} }

View File

@ -11,6 +11,8 @@ namespace osu.Game.Rulesets.Edit
{ {
protected readonly OsuScrollContainer Scroll; protected readonly OsuScrollContainer Scroll;
protected readonly FillFlowContainer FillFlow;
protected override Container<Drawable> Content { get; } protected override Container<Drawable> Content { get; }
public ScrollingToolboxGroup(string title, float scrollAreaHeight) public ScrollingToolboxGroup(string title, float scrollAreaHeight)
@ -20,7 +22,7 @@ namespace osu.Game.Rulesets.Edit
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Height = scrollAreaHeight, Height = scrollAreaHeight,
Child = Content = new FillFlowContainer Child = Content = FillFlow = new FillFlowContainer
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,

View File

@ -192,7 +192,7 @@ namespace osu.Game.Rulesets.Mods
hashCode.Add(GetType()); hashCode.Add(GetType());
foreach (var setting in Settings) foreach (var setting in Settings)
hashCode.Add(ModUtils.GetSettingUnderlyingValue(setting)); hashCode.Add(setting.GetUnderlyingSettingValue());
return hashCode.ToHashCode(); return hashCode.ToHashCode();
} }
@ -208,13 +208,13 @@ namespace osu.Game.Rulesets.Mods
public bool Equals(IBindable x, IBindable y) public bool Equals(IBindable x, IBindable y)
{ {
object xValue = x == null ? null : ModUtils.GetSettingUnderlyingValue(x); object xValue = x?.GetUnderlyingSettingValue();
object yValue = y == null ? null : ModUtils.GetSettingUnderlyingValue(y); object yValue = y?.GetUnderlyingSettingValue();
return EqualityComparer<object>.Default.Equals(xValue, yValue); return EqualityComparer<object>.Default.Equals(xValue, yValue);
} }
public int GetHashCode(IBindable obj) => ModUtils.GetSettingUnderlyingValue(obj).GetHashCode(); public int GetHashCode(IBindable obj) => obj.GetUnderlyingSettingValue().GetHashCode();
} }
} }
} }

View File

@ -4,9 +4,12 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Humanizer;
using Newtonsoft.Json; using Newtonsoft.Json;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Configuration;
using osu.Game.Extensions; using osu.Game.Extensions;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK; using osuTK;
@ -34,6 +37,8 @@ namespace osu.Game.Screens.Play.HUD
/// <inheritdoc cref="ISkinnableDrawable.UsesFixedAnchor"/> /// <inheritdoc cref="ISkinnableDrawable.UsesFixedAnchor"/>
public bool UsesFixedAnchor { get; set; } public bool UsesFixedAnchor { get; set; }
public Dictionary<string, object> Settings { get; set; } = new Dictionary<string, object>();
public List<SkinnableInfo> Children { get; } = new List<SkinnableInfo>(); public List<SkinnableInfo> Children { get; } = new List<SkinnableInfo>();
[JsonConstructor] [JsonConstructor]
@ -58,6 +63,14 @@ namespace osu.Game.Screens.Play.HUD
if (component is ISkinnableDrawable skinnable) if (component is ISkinnableDrawable skinnable)
UsesFixedAnchor = skinnable.UsesFixedAnchor; UsesFixedAnchor = skinnable.UsesFixedAnchor;
foreach (var (_, property) in component.GetSettingsSourceProperties())
{
var bindable = (IBindable)property.GetValue(component);
if (!bindable.IsDefault)
Settings.Add(property.Name.Underscore(), bindable.GetUnderlyingSettingValue());
}
if (component is Container<Drawable> container) if (component is Container<Drawable> container)
{ {
foreach (var child in container.OfType<ISkinnableDrawable>().OfType<Drawable>()) foreach (var child in container.OfType<ISkinnableDrawable>().OfType<Drawable>())

View File

@ -0,0 +1,92 @@
// 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 JetBrains.Annotations;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Skinning.Components
{
/// <summary>
/// Intended to be a test bed for skinning. May be removed at some point in the future.
/// </summary>
[UsedImplicitly]
public class BigBlackBox : CompositeDrawable, ISkinnableDrawable
{
public bool UsesFixedAnchor { get; set; }
[SettingSource("Spinning text", "Whether the big text should spin")]
public Bindable<bool> TextSpin { get; } = new BindableBool();
[SettingSource("Alpha", "The alpha value of this box")]
public BindableNumber<float> BoxAlpha { get; } = new BindableNumber<float>(1)
{
MinValue = 0,
MaxValue = 1,
Precision = 0.01f,
};
private readonly Box box;
private readonly OsuSpriteText text;
private readonly OsuTextFlowContainer disclaimer;
public BigBlackBox()
{
Size = new Vector2(250);
Masking = true;
CornerRadius = 20;
CornerExponent = 5;
InternalChildren = new Drawable[]
{
box = new Box
{
Colour = Color4.Black,
RelativeSizeAxes = Axes.Both,
},
text = new OsuSpriteText
{
Text = "Big Black Box",
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = OsuFont.Default.With(size: 40)
},
disclaimer = new OsuTextFlowContainer(st => st.Font = OsuFont.Default.With(size: 10))
{
Text = "This is intended to be a test component and may disappear in the future!",
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Margin = new MarginPadding(10),
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
TextAnchor = Anchor.TopCentre,
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
BoxAlpha.BindValueChanged(alpha => box.Alpha = alpha.NewValue, true);
TextSpin.BindValueChanged(spin =>
{
if (spin.NewValue)
text.Spin(1000, RotationDirection.Clockwise);
else
text.ClearTransforms();
}, true);
disclaimer.FadeOutFromOne(5000, Easing.InQuint);
}
}
}

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
@ -11,10 +12,12 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Configuration;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Cursor; using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
using osu.Game.Screens.Edit.Components.Menus; using osu.Game.Screens.Edit.Components.Menus;
namespace osu.Game.Skinning.Editor namespace osu.Game.Skinning.Editor
@ -44,6 +47,8 @@ namespace osu.Game.Skinning.Editor
private Container content; private Container content;
private EditorToolboxGroup settingsToolbox;
public SkinEditor(Drawable targetScreen) public SkinEditor(Drawable targetScreen)
{ {
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
@ -103,7 +108,8 @@ namespace osu.Game.Skinning.Editor
ColumnDimensions = new[] ColumnDimensions = new[]
{ {
new Dimension(GridSizeMode.AutoSize), new Dimension(GridSizeMode.AutoSize),
new Dimension() new Dimension(),
new Dimension(GridSizeMode.AutoSize),
}, },
Content = new[] Content = new[]
{ {
@ -119,6 +125,11 @@ namespace osu.Game.Skinning.Editor
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
}, },
settingsToolbox = new SkinSettingsToolbox
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
}
} }
} }
} }
@ -143,12 +154,15 @@ namespace osu.Game.Skinning.Editor
hasBegunMutating = false; hasBegunMutating = false;
Scheduler.AddOnce(skinChanged); Scheduler.AddOnce(skinChanged);
}, true); }, true);
SelectedComponents.BindCollectionChanged(selectionChanged);
} }
public void UpdateTargetScreen(Drawable targetScreen) public void UpdateTargetScreen(Drawable targetScreen)
{ {
this.targetScreen = targetScreen; this.targetScreen = targetScreen;
SelectedComponents.Clear();
Scheduler.AddOnce(loadBlueprintContainer); Scheduler.AddOnce(loadBlueprintContainer);
void loadBlueprintContainer() void loadBlueprintContainer()
@ -210,6 +224,18 @@ namespace osu.Game.Skinning.Editor
SelectedComponents.Add(component); SelectedComponents.Add(component);
} }
private void selectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
settingsToolbox.Clear();
var first = SelectedComponents.OfType<Drawable>().FirstOrDefault();
if (first != null)
{
settingsToolbox.Children = first.CreateSettingsControls().ToArray();
}
}
private IEnumerable<ISkinnableTarget> availableTargets => targetScreen.ChildrenOfType<ISkinnableTarget>(); private IEnumerable<ISkinnableTarget> availableTargets => targetScreen.ChildrenOfType<ISkinnableTarget>();
private ISkinnableTarget getTarget(SkinnableTarget target) private ISkinnableTarget getTarget(SkinnableTarget target)

View File

@ -101,9 +101,11 @@ namespace osu.Game.Skinning.Editor
private void editorVisibilityChanged(ValueChangedEvent<Visibility> visibility) private void editorVisibilityChanged(ValueChangedEvent<Visibility> visibility)
{ {
const float toolbar_padding_requirement = 0.18f;
if (visibility.NewValue == Visibility.Visible) if (visibility.NewValue == Visibility.Visible)
{ {
target.SetCustomRect(new RectangleF(0.18f, 0.1f, VISIBLE_TARGET_SCALE, VISIBLE_TARGET_SCALE), true); target.SetCustomRect(new RectangleF(toolbar_padding_requirement, 0.1f, 0.8f - toolbar_padding_requirement, 0.7f), true);
} }
else else
{ {

View File

@ -0,0 +1,23 @@
// 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 osu.Framework.Graphics;
using osu.Game.Rulesets.Edit;
using osuTK;
namespace osu.Game.Skinning.Editor
{
internal class SkinSettingsToolbox : ScrollingToolboxGroup
{
public const float WIDTH = 200;
public SkinSettingsToolbox()
: base("Settings", 600)
{
RelativeSizeAxes = Axes.None;
Width = WIDTH;
FillFlow.Spacing = new Vector2(10);
}
}
}

View File

@ -1,6 +1,9 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Bindables;
using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
namespace osu.Game.Skinning namespace osu.Game.Skinning
@ -21,5 +24,22 @@ namespace osu.Game.Skinning
/// If <see langword="true"/>, a fixed anchor point has been defined. /// If <see langword="true"/>, a fixed anchor point has been defined.
/// </summary> /// </summary>
bool UsesFixedAnchor { get; set; } bool UsesFixedAnchor { get; set; }
void CopyAdjustedSetting(IBindable target, object source)
{
if (source is IBindable sourceBindable)
{
// copy including transfer of default values.
target.BindTo(sourceBindable);
target.UnbindFrom(sourceBindable);
}
else
{
if (!(target is IParseable parseable))
throw new InvalidOperationException($"Bindable type {target.GetType().ReadableName()} is not {nameof(IParseable)}.");
parseable.Parse(source);
}
}
} }
} }

View File

@ -1,18 +1,16 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
#nullable enable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using osu.Framework.Bindables;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
#nullable enable
namespace osu.Game.Utils namespace osu.Game.Utils
{ {
/// <summary> /// <summary>
@ -154,39 +152,6 @@ namespace osu.Game.Utils
yield return mod; yield return mod;
} }
/// <summary>
/// Returns the underlying value of the given mod setting object.
/// Used in <see cref="APIMod"/> for serialization and equality comparison purposes.
/// </summary>
/// <param name="setting">The mod setting.</param>
public static object GetSettingUnderlyingValue(object setting)
{
switch (setting)
{
case Bindable<double> d:
return d.Value;
case Bindable<int> i:
return i.Value;
case Bindable<float> f:
return f.Value;
case Bindable<bool> b:
return b.Value;
case IBindable u:
// A mod with unknown (e.g. enum) generic type.
var valueMethod = u.GetType().GetProperty(nameof(IBindable<int>.Value));
Debug.Assert(valueMethod != null);
return valueMethod.GetValue(u);
default:
// fall back for non-bindable cases.
return setting;
}
}
/// <summary> /// <summary>
/// Verifies all proposed mods are valid for a given ruleset and returns instantiated <see cref="Mod"/>s for further processing. /// Verifies all proposed mods are valid for a given ruleset and returns instantiated <see cref="Mod"/>s for further processing.
/// </summary> /// </summary>