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:
commit
d80830b415
@ -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();
|
||||
|
@ -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)
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>())
|
||||
|
92
osu.Game/Skinning/Components/BigBlackBox.cs
Normal file
92
osu.Game/Skinning/Components/BigBlackBox.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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
|
||||
{
|
||||
|
23
osu.Game/Skinning/Editor/SkinSettingsToolbox.cs
Normal file
23
osu.Game/Skinning/Editor/SkinSettingsToolbox.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
Loading…
x
Reference in New Issue
Block a user