1
0
mirror of https://github.com/ppy/osu.git synced 2025-03-15 15:27:20 +08:00

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
commit d80830b415
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 240 additions and 51 deletions

View File

@ -6,6 +6,7 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
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)[]>();
/// <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)
{
var type = obj.GetType();

View File

@ -1,8 +1,11 @@
// 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 Humanizer;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Configuration;
using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning;
using osuTK;
@ -59,8 +62,18 @@ namespace osu.Game.Extensions
component.Origin = info.Origin;
if (component is ISkinnableDrawable skinnable)
{
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)
{
foreach (var child in info.Children)

View File

@ -12,7 +12,6 @@ using osu.Framework.Logging;
using osu.Game.Configuration;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Utils;
namespace osu.Game.Online.API
{
@ -43,7 +42,7 @@ namespace osu.Game.Online.API
var bindable = (IBindable)property.GetValue(mod);
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)
{
object xValue = ModUtils.GetSettingUnderlyingValue(x.Value);
object yValue = ModUtils.GetSettingUnderlyingValue(y.Value);
object xValue = x.Value.GetUnderlyingSettingValue();
object yValue = y.Value.GetUnderlyingSettingValue();
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 MessagePack;
using MessagePack.Formatters;
using osu.Game.Utils;
using osu.Game.Configuration;
namespace osu.Game.Online.API
{
@ -23,7 +23,7 @@ namespace osu.Game.Online.API
var stringBytes = new ReadOnlySequence<byte>(Encoding.UTF8.GetBytes(kvp.Key));
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 FillFlowContainer FillFlow;
protected override Container<Drawable> Content { get; }
public ScrollingToolboxGroup(string title, float scrollAreaHeight)
@ -20,7 +22,7 @@ namespace osu.Game.Rulesets.Edit
{
RelativeSizeAxes = Axes.X,
Height = scrollAreaHeight,
Child = Content = new FillFlowContainer
Child = Content = FillFlow = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,

View File

@ -192,7 +192,7 @@ namespace osu.Game.Rulesets.Mods
hashCode.Add(GetType());
foreach (var setting in Settings)
hashCode.Add(ModUtils.GetSettingUnderlyingValue(setting));
hashCode.Add(setting.GetUnderlyingSettingValue());
return hashCode.ToHashCode();
}
@ -208,13 +208,13 @@ namespace osu.Game.Rulesets.Mods
public bool Equals(IBindable x, IBindable y)
{
object xValue = x == null ? null : ModUtils.GetSettingUnderlyingValue(x);
object yValue = y == null ? null : ModUtils.GetSettingUnderlyingValue(y);
object xValue = x?.GetUnderlyingSettingValue();
object yValue = y?.GetUnderlyingSettingValue();
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.Collections.Generic;
using System.Linq;
using Humanizer;
using Newtonsoft.Json;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Configuration;
using osu.Game.Extensions;
using osu.Game.Skinning;
using osuTK;
@ -34,6 +37,8 @@ namespace osu.Game.Screens.Play.HUD
/// <inheritdoc cref="ISkinnableDrawable.UsesFixedAnchor"/>
public bool UsesFixedAnchor { get; set; }
public Dictionary<string, object> Settings { get; set; } = new Dictionary<string, object>();
public List<SkinnableInfo> Children { get; } = new List<SkinnableInfo>();
[JsonConstructor]
@ -58,6 +63,14 @@ namespace osu.Game.Screens.Play.HUD
if (component is ISkinnableDrawable skinnable)
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)
{
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.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@ -11,10 +12,12 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Framework.Testing;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
using osu.Game.Screens.Edit.Components.Menus;
namespace osu.Game.Skinning.Editor
@ -44,6 +47,8 @@ namespace osu.Game.Skinning.Editor
private Container content;
private EditorToolboxGroup settingsToolbox;
public SkinEditor(Drawable targetScreen)
{
RelativeSizeAxes = Axes.Both;
@ -103,7 +108,8 @@ namespace osu.Game.Skinning.Editor
ColumnDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize),
new Dimension()
new Dimension(),
new Dimension(GridSizeMode.AutoSize),
},
Content = new[]
{
@ -119,6 +125,11 @@ namespace osu.Game.Skinning.Editor
{
RelativeSizeAxes = Axes.Both,
},
settingsToolbox = new SkinSettingsToolbox
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
}
}
}
}
@ -143,12 +154,15 @@ namespace osu.Game.Skinning.Editor
hasBegunMutating = false;
Scheduler.AddOnce(skinChanged);
}, true);
SelectedComponents.BindCollectionChanged(selectionChanged);
}
public void UpdateTargetScreen(Drawable targetScreen)
{
this.targetScreen = targetScreen;
SelectedComponents.Clear();
Scheduler.AddOnce(loadBlueprintContainer);
void loadBlueprintContainer()
@ -210,6 +224,18 @@ namespace osu.Game.Skinning.Editor
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 ISkinnableTarget getTarget(SkinnableTarget target)

View File

@ -101,9 +101,11 @@ namespace osu.Game.Skinning.Editor
private void editorVisibilityChanged(ValueChangedEvent<Visibility> visibility)
{
const float toolbar_padding_requirement = 0.18f;
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
{

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.
// 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;
namespace osu.Game.Skinning
@ -21,5 +24,22 @@ namespace osu.Game.Skinning
/// If <see langword="true"/>, a fixed anchor point has been defined.
/// </summary>
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.
// See the LICENCE file in the repository root for full licence text.
#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using osu.Framework.Bindables;
using osu.Game.Online.API;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
#nullable enable
namespace osu.Game.Utils
{
/// <summary>
@ -154,39 +152,6 @@ namespace osu.Game.Utils
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>
/// Verifies all proposed mods are valid for a given ruleset and returns instantiated <see cref="Mod"/>s for further processing.
/// </summary>