1
0
mirror of https://github.com/ppy/osu.git synced 2025-02-21 03:02:54 +08:00

Merge pull request #12750 from peppy/skin-serialisation

Add skin editor saving / loading support
This commit is contained in:
Dan Balasescu 2021-05-13 20:58:03 +09:00 committed by GitHub
commit 746862dcb1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 878 additions and 256 deletions

View File

@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
explosion = new LegacyRollingCounter(skin, LegacyFont.Combo) explosion = new LegacyRollingCounter(LegacyFont.Combo)
{ {
Alpha = 0.65f, Alpha = 0.65f,
Blending = BlendingParameters.Additive, Blending = BlendingParameters.Additive,
@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
Origin = Anchor.Centre, Origin = Anchor.Centre,
Scale = new Vector2(1.5f), Scale = new Vector2(1.5f),
}, },
counter = new LegacyRollingCounter(skin, LegacyFont.Combo) counter = new LegacyRollingCounter(LegacyFont.Combo)
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,

View File

@ -76,7 +76,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
Scale = new Vector2(SPRITE_SCALE), Scale = new Vector2(SPRITE_SCALE),
Y = SPINNER_TOP_OFFSET + 115, Y = SPINNER_TOP_OFFSET + 115,
}, },
bonusCounter = new LegacySpriteText(source, LegacyFont.Score) bonusCounter = new LegacySpriteText(LegacyFont.Score)
{ {
Alpha = 0f, Alpha = 0f,
Anchor = Anchor.TopCentre, Anchor = Anchor.TopCentre,
@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
Scale = new Vector2(SPRITE_SCALE), Scale = new Vector2(SPRITE_SCALE),
Position = new Vector2(-87, 445 + spm_hide_offset), Position = new Vector2(-87, 445 + spm_hide_offset),
}, },
spmCounter = new LegacySpriteText(source, LegacyFont.Score) spmCounter = new LegacySpriteText(LegacyFont.Score)
{ {
Anchor = Anchor.TopCentre, Anchor = Anchor.TopCentre,
Origin = Anchor.TopRight, Origin = Anchor.TopRight,

View File

@ -100,7 +100,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
if (!this.HasFont(LegacyFont.HitCircle)) if (!this.HasFont(LegacyFont.HitCircle))
return null; return null;
return new LegacySpriteText(Source, LegacyFont.HitCircle) return new LegacySpriteText(LegacyFont.HitCircle)
{ {
// stable applies a blanket 0.8x scale to hitcircle fonts // stable applies a blanket 0.8x scale to hitcircle fonts
Scale = new Vector2(0.8f), Scale = new Vector2(0.8f),

View File

@ -2,10 +2,12 @@
// 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 NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Skinning;
using osu.Game.Skinning.Editor; using osu.Game.Skinning.Editor;
namespace osu.Game.Tests.Visual.Gameplay namespace osu.Game.Tests.Visual.Gameplay
@ -14,12 +16,17 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
private SkinEditor skinEditor; private SkinEditor skinEditor;
[Resolved]
private SkinManager skinManager { get; set; }
protected override bool Autoplay => true;
[SetUpSteps] [SetUpSteps]
public override void SetUpSteps() public override void SetUpSteps()
{ {
base.SetUpSteps(); base.SetUpSteps();
AddStep("add editor overlay", () => AddStep("reload skin editor", () =>
{ {
skinEditor?.Expire(); skinEditor?.Expire();
Player.ScaleTo(SkinEditorOverlay.VISIBLE_TARGET_SCALE); Player.ScaleTo(SkinEditorOverlay.VISIBLE_TARGET_SCALE);

View File

@ -1,13 +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 System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
@ -32,12 +30,13 @@ namespace osu.Game.Tests.Visual.Gameplay
SetContents(() => SetContents(() =>
{ {
var ruleset = new OsuRuleset(); var ruleset = new OsuRuleset();
var mods = new[] { ruleset.GetAutoplayMod() };
var working = CreateWorkingBeatmap(ruleset.RulesetInfo); var working = CreateWorkingBeatmap(ruleset.RulesetInfo);
var beatmap = working.GetPlayableBeatmap(ruleset.RulesetInfo); var beatmap = working.GetPlayableBeatmap(ruleset.RulesetInfo, mods);
var drawableRuleset = ruleset.CreateDrawableRulesetWith(beatmap); var drawableRuleset = ruleset.CreateDrawableRulesetWith(beatmap, mods);
var hudOverlay = new HUDOverlay(drawableRuleset, Array.Empty<Mod>()) var hudOverlay = new HUDOverlay(drawableRuleset, mods)
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,

View File

@ -472,7 +472,7 @@ namespace osu.Game.Database
} }
/// <summary> /// <summary>
/// Delete new file. /// Delete an existing file.
/// </summary> /// </summary>
/// <param name="model">The item to operate on.</param> /// <param name="model">The item to operate on.</param>
/// <param name="file">The existing file to be deleted.</param> /// <param name="file">The existing file to be deleted.</param>

View File

@ -3,8 +3,10 @@
using System; using System;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Game.Screens.Play.HUD;
using osuTK; using osuTK;
namespace osu.Game.Extensions namespace osu.Game.Extensions
@ -43,5 +45,23 @@ namespace osu.Game.Extensions
/// <returns>The delta vector in Parent's coordinates.</returns> /// <returns>The delta vector in Parent's coordinates.</returns>
public static Vector2 ScreenSpaceDeltaToParentSpace(this Drawable drawable, Vector2 delta) => public static Vector2 ScreenSpaceDeltaToParentSpace(this Drawable drawable, Vector2 delta) =>
drawable.Parent.ToLocalSpace(drawable.Parent.ToScreenSpace(Vector2.Zero) + delta); drawable.Parent.ToLocalSpace(drawable.Parent.ToScreenSpace(Vector2.Zero) + delta);
public static SkinnableInfo CreateSkinnableInfo(this Drawable component) => new SkinnableInfo(component);
public static void ApplySkinnableInfo(this Drawable component, SkinnableInfo info)
{
// todo: can probably make this better via deserialisation directly using a common interface.
component.Position = info.Position;
component.Rotation = info.Rotation;
component.Scale = info.Scale;
component.Anchor = info.Anchor;
component.Origin = info.Origin;
if (component is Container container)
{
foreach (var child in info.Children)
container.Add(child.CreateInstance());
}
}
} }
} }

View File

@ -0,0 +1,18 @@
// 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.Allocation;
namespace osu.Game.Graphics.UserInterface
{
public class DangerousTriangleButton : TriangleButton
{
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
BackgroundColour = colours.PinkDark;
Triangles.ColourDark = colours.PinkDarker;
Triangles.ColourLight = colours.Pink;
}
}
}

View File

@ -339,22 +339,13 @@ namespace osu.Game.Overlays.KeyBinding
} }
} }
public class ClearButton : TriangleButton public class ClearButton : DangerousTriangleButton
{ {
public ClearButton() public ClearButton()
{ {
Text = "Clear"; Text = "Clear";
Size = new Vector2(80, 20); Size = new Vector2(80, 20);
} }
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
BackgroundColour = colours.Pink;
Triangles.ColourDark = colours.PinkDark;
Triangles.ColourLight = colours.PinkLight;
}
} }
public class KeyButton : Container public class KeyButton : Container

View File

@ -11,7 +11,6 @@ using osu.Game.Input;
using osu.Game.Overlays.Settings; using osu.Game.Overlays.Settings;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osuTK; using osuTK;
using osu.Game.Graphics;
namespace osu.Game.Overlays.KeyBinding namespace osu.Game.Overlays.KeyBinding
{ {
@ -55,10 +54,10 @@ namespace osu.Game.Overlays.KeyBinding
} }
} }
public class ResetButton : TriangleButton public class ResetButton : DangerousTriangleButton
{ {
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours) private void load()
{ {
Text = "Reset all bindings in section"; Text = "Reset all bindings in section";
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
@ -66,10 +65,6 @@ namespace osu.Game.Overlays.KeyBinding
Height = 20; Height = 20;
Content.CornerRadius = 5; Content.CornerRadius = 5;
BackgroundColour = colours.PinkDark;
Triangles.ColourDark = colours.PinkDarker;
Triangles.ColourLight = colours.Pink;
} }
} }
} }

View File

@ -3,9 +3,11 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Primitives;
@ -24,6 +26,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// Includes selection and manipulation support via a <see cref="Components.SelectionHandler{T}"/>. /// Includes selection and manipulation support via a <see cref="Components.SelectionHandler{T}"/>.
/// </summary> /// </summary>
public abstract class BlueprintContainer<T> : CompositeDrawable, IKeyBindingHandler<PlatformAction> public abstract class BlueprintContainer<T> : CompositeDrawable, IKeyBindingHandler<PlatformAction>
where T : class
{ {
protected DragBox DragBox { get; private set; } protected DragBox DragBox { get; private set; }
@ -39,6 +42,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
private IEditorChangeHandler changeHandler { get; set; } private IEditorChangeHandler changeHandler { get; set; }
protected readonly BindableList<T> SelectedItems = new BindableList<T>();
protected BlueprintContainer() protected BlueprintContainer()
{ {
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
@ -47,6 +52,24 @@ namespace osu.Game.Screens.Edit.Compose.Components
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
SelectedItems.CollectionChanged += (selectedObjects, args) =>
{
switch (args.Action)
{
case NotifyCollectionChangedAction.Add:
foreach (var o in args.NewItems)
SelectionBlueprints.FirstOrDefault(b => b.Item == o)?.Select();
break;
case NotifyCollectionChangedAction.Remove:
foreach (var o in args.OldItems)
SelectionBlueprints.FirstOrDefault(b => b.Item == o)?.Deselect();
break;
}
};
SelectionHandler = CreateSelectionHandler(); SelectionHandler = CreateSelectionHandler();
SelectionHandler.DeselectAll = deselectAll; SelectionHandler.DeselectAll = deselectAll;

View File

@ -2,10 +2,8 @@
// 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.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.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
@ -24,8 +22,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
protected readonly HitObjectComposer Composer; protected readonly HitObjectComposer Composer;
private readonly BindableList<HitObject> selectedHitObjects = new BindableList<HitObject>();
protected EditorBlueprintContainer(HitObjectComposer composer) protected EditorBlueprintContainer(HitObjectComposer composer)
{ {
Composer = composer; Composer = composer;
@ -34,23 +30,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
selectedHitObjects.BindTo(Beatmap.SelectedHitObjects); SelectedItems.BindTo(Beatmap.SelectedHitObjects);
selectedHitObjects.CollectionChanged += (selectedObjects, args) =>
{
switch (args.Action)
{
case NotifyCollectionChangedAction.Add:
foreach (var o in args.NewItems)
SelectionBlueprints.FirstOrDefault(b => b.Item == o)?.Select();
break;
case NotifyCollectionChangedAction.Remove:
foreach (var o in args.OldItems)
SelectionBlueprints.FirstOrDefault(b => b.Item == o)?.Deselect();
break;
}
};
} }
protected override void LoadComplete() protected override void LoadComplete()

View File

@ -2,23 +2,13 @@
// 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 osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Screens.Play.HUD namespace osu.Game.Screens.Play.HUD
{ {
public class DefaultAccuracyCounter : GameplayAccuracyCounter, ISkinnableComponent public class DefaultAccuracyCounter : GameplayAccuracyCounter, ISkinnableDrawable
{ {
private readonly Vector2 offset = new Vector2(-20, 5);
public DefaultAccuracyCounter()
{
Origin = Anchor.TopRight;
Anchor = Anchor.TopRight;
}
[Resolved(canBeNull: true)] [Resolved(canBeNull: true)]
private HUDOverlay hud { get; set; } private HUDOverlay hud { get; set; }
@ -27,17 +17,5 @@ namespace osu.Game.Screens.Play.HUD
{ {
Colour = colours.BlueLighter; Colour = colours.BlueLighter;
} }
protected override void Update()
{
base.Update();
if (hud?.ScoreCounter.Drawable is DefaultScoreCounter score)
{
// for now align with the score counter. eventually this will be user customisable.
Anchor = Anchor.TopLeft;
Position = Parent.ToLocalSpace(score.ScreenSpaceDrawQuad.TopLeft) + offset;
}
}
} }
} }

View File

@ -9,14 +9,11 @@ using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Screens.Play.HUD namespace osu.Game.Screens.Play.HUD
{ {
public class DefaultComboCounter : RollingCounter<int>, ISkinnableComponent public class DefaultComboCounter : RollingCounter<int>, ISkinnableDrawable
{ {
private readonly Vector2 offset = new Vector2(20, 5);
[Resolved(canBeNull: true)] [Resolved(canBeNull: true)]
private HUDOverlay hud { get; set; } private HUDOverlay hud { get; set; }
@ -32,17 +29,6 @@ namespace osu.Game.Screens.Play.HUD
Current.BindTo(scoreProcessor.Combo); Current.BindTo(scoreProcessor.Combo);
} }
protected override void Update()
{
base.Update();
if (hud?.ScoreCounter.Drawable is DefaultScoreCounter score)
{
// for now align with the score counter. eventually this will be user customisable.
Position = Parent.ToLocalSpace(score.ScreenSpaceDrawQuad.TopRight) + offset;
}
}
protected override string FormatCount(int count) protected override string FormatCount(int count)
{ {
return $@"{count}x"; return $@"{count}x";

View File

@ -17,7 +17,7 @@ using osu.Game.Skinning;
namespace osu.Game.Screens.Play.HUD namespace osu.Game.Screens.Play.HUD
{ {
public class DefaultHealthDisplay : HealthDisplay, IHasAccentColour, ISkinnableComponent public class DefaultHealthDisplay : HealthDisplay, IHasAccentColour, ISkinnableDrawable
{ {
/// <summary> /// <summary>
/// The base opacity of the glow. /// The base opacity of the glow.

View File

@ -8,7 +8,7 @@ using osu.Game.Skinning;
namespace osu.Game.Screens.Play.HUD namespace osu.Game.Screens.Play.HUD
{ {
public class DefaultScoreCounter : GameplayScoreCounter, ISkinnableComponent public class DefaultScoreCounter : GameplayScoreCounter, ISkinnableDrawable
{ {
public DefaultScoreCounter() public DefaultScoreCounter()
: base(6) : base(6)
@ -24,12 +24,6 @@ namespace osu.Game.Screens.Play.HUD
private void load(OsuColour colours) private void load(OsuColour colours)
{ {
Colour = colours.BlueLighter; Colour = colours.BlueLighter;
// todo: check if default once health display is skinnable
hud?.ShowHealthbar.BindValueChanged(healthBar =>
{
this.MoveToY(healthBar.NewValue ? 30 : 0, HUDOverlay.FADE_DURATION, HUDOverlay.FADE_EASING);
}, true);
} }
} }
} }

View File

@ -13,13 +13,12 @@ using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
namespace osu.Game.Screens.Play.HUD.HitErrorMeters namespace osu.Game.Screens.Play.HUD.HitErrorMeters
{ {
public class BarHitErrorMeter : HitErrorMeter, ISkinnableComponent public class BarHitErrorMeter : HitErrorMeter
{ {
private readonly Anchor alignment; private readonly Anchor alignment;

View File

@ -15,7 +15,7 @@ namespace osu.Game.Screens.Play.HUD
/// <summary> /// <summary>
/// Uses the 'x' symbol and has a pop-out effect while rolling over. /// Uses the 'x' symbol and has a pop-out effect while rolling over.
/// </summary> /// </summary>
public class LegacyComboCounter : CompositeDrawable, ISkinnableComponent public class LegacyComboCounter : CompositeDrawable, ISkinnableDrawable
{ {
public Bindable<int> Current { get; } = new BindableInt { MinValue = 0, }; public Bindable<int> Current { get; } = new BindableInt { MinValue = 0, };
@ -84,13 +84,13 @@ namespace osu.Game.Screens.Play.HUD
{ {
InternalChildren = new[] InternalChildren = new[]
{ {
popOutCount = new LegacySpriteText(skin, LegacyFont.Combo) popOutCount = new LegacySpriteText(LegacyFont.Combo)
{ {
Alpha = 0, Alpha = 0,
Margin = new MarginPadding(0.05f), Margin = new MarginPadding(0.05f),
Blending = BlendingParameters.Additive, Blending = BlendingParameters.Additive,
}, },
displayedCountSpriteText = new LegacySpriteText(skin, LegacyFont.Combo) displayedCountSpriteText = new LegacySpriteText(LegacyFont.Combo)
{ {
Alpha = 0, Alpha = 0,
}, },

View File

@ -0,0 +1,74 @@
// 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.Linq;
using Newtonsoft.Json;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Extensions;
using osu.Game.IO.Serialization;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Screens.Play.HUD
{
/// <summary>
/// Serialised information governing custom changes to an <see cref="ISkinnableDrawable"/>.
/// </summary>
[Serializable]
public class SkinnableInfo : IJsonSerializable
{
public Type Type { get; set; }
public Vector2 Position { get; set; }
public float Rotation { get; set; }
public Vector2 Scale { get; set; }
public Anchor Anchor { get; set; }
public Anchor Origin { get; set; }
public List<SkinnableInfo> Children { get; } = new List<SkinnableInfo>();
[JsonConstructor]
public SkinnableInfo()
{
}
/// <summary>
/// Construct a new instance populating all attributes from the provided drawable.
/// </summary>
/// <param name="component">The drawable which attributes should be sourced from.</param>
public SkinnableInfo(Drawable component)
{
Type = component.GetType();
Position = component.Position;
Rotation = component.Rotation;
Scale = component.Scale;
Anchor = component.Anchor;
Origin = component.Origin;
if (component is Container<Drawable> container)
{
foreach (var child in container.OfType<ISkinnableDrawable>().OfType<Drawable>())
Children.Add(child.CreateSkinnableInfo());
}
}
/// <summary>
/// Construct an instance of the drawable with all attributes applied.
/// </summary>
/// <returns>The new instance.</returns>
public Drawable CreateInstance()
{
Drawable d = (Drawable)Activator.CreateInstance(Type);
d.ApplySkinnableInfo(this);
return d;
}
}
}

View File

@ -3,8 +3,10 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
@ -22,7 +24,7 @@ using osuTK;
namespace osu.Game.Screens.Play namespace osu.Game.Screens.Play
{ {
[Cached] [Cached]
public class HUDOverlay : Container, IKeyBindingHandler<GlobalAction>, IDefaultSkinnableTarget public class HUDOverlay : Container, IKeyBindingHandler<GlobalAction>
{ {
public const float FADE_DURATION = 300; public const float FADE_DURATION = 300;
@ -34,8 +36,6 @@ namespace osu.Game.Screens.Play
public float TopScoringElementsHeight { get; private set; } public float TopScoringElementsHeight { get; private set; }
public readonly KeyCounterDisplay KeyCounter; public readonly KeyCounterDisplay KeyCounter;
public readonly SkinnableScoreCounter ScoreCounter;
public readonly SkinnableAccuracyCounter AccuracyCounter;
public readonly SongProgress Progress; public readonly SongProgress Progress;
public readonly ModDisplay ModDisplay; public readonly ModDisplay ModDisplay;
public readonly HoldForMenuButton HoldToQuit; public readonly HoldForMenuButton HoldToQuit;
@ -68,6 +68,8 @@ namespace osu.Game.Screens.Play
private bool holdingForHUD; private bool holdingForHUD;
private readonly SkinnableTargetContainer mainComponents;
private IEnumerable<Drawable> hideTargets => new Drawable[] { visibilityContainer, KeyCounter, topRightElements }; private IEnumerable<Drawable> hideTargets => new Drawable[] { visibilityContainer, KeyCounter, topRightElements };
public HUDOverlay(DrawableRuleset drawableRuleset, IReadOnlyList<Mod> mods) public HUDOverlay(DrawableRuleset drawableRuleset, IReadOnlyList<Mod> mods)
@ -95,11 +97,19 @@ namespace osu.Game.Screens.Play
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Children = new Drawable[] Children = new Drawable[]
{ {
CreateHealthDisplay(), mainComponents = new SkinnableTargetContainer(SkinnableTarget.MainHUDComponents)
AccuracyCounter = CreateAccuracyCounter(), {
ScoreCounter = CreateScoreCounter(), RelativeSizeAxes = Axes.Both,
CreateComboCounter(), },
CreateHitErrorDisplayOverlay(), new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
// still need to be migrated; a bit more involved.
new HitErrorDisplay(this.drawableRuleset?.FirstAvailableHitWindows),
}
},
} }
}, },
}, },
@ -196,11 +206,25 @@ namespace osu.Game.Screens.Play
{ {
base.Update(); base.Update();
// HACK: for now align with the accuracy counter. Vector2 lowestScreenSpace = Vector2.Zero;
// this is done for the sake of hacky legacy skins which extend the health bar to take up the full screen area.
// it only works with the default skin due to padding offsetting it *just enough* to coexist.
topRightElements.Y = TopScoringElementsHeight = ToLocalSpace(AccuracyCounter.Drawable.ScreenSpaceDrawQuad.BottomRight).Y;
// LINQ cast can be removed when IDrawable interface includes Anchor / RelativeSizeAxes.
foreach (var element in mainComponents.Components.Cast<Drawable>())
{
// for now align top-right components with the bottom-edge of the lowest top-anchored hud element.
if (!element.Anchor.HasFlagFast(Anchor.TopRight) && !element.RelativeSizeAxes.HasFlagFast(Axes.X))
continue;
// health bars are excluded for the sake of hacky legacy skins which extend the health bar to take up the full screen area.
if (element is LegacyHealthDisplay)
continue;
var bottomRight = element.ScreenSpaceDrawQuad.BottomRight;
if (bottomRight.Y > lowestScreenSpace.Y)
lowestScreenSpace = bottomRight;
}
topRightElements.Y = TopScoringElementsHeight = ToLocalSpace(lowestScreenSpace).Y;
bottomRightElements.Y = -Progress.Height; bottomRightElements.Y = -Progress.Height;
} }
@ -261,48 +285,38 @@ namespace osu.Game.Screens.Play
Progress.BindDrawableRuleset(drawableRuleset); Progress.BindDrawableRuleset(drawableRuleset);
} }
protected SkinnableAccuracyCounter CreateAccuracyCounter() => new SkinnableAccuracyCounter(); protected FailingLayer CreateFailingLayer() => new FailingLayer
protected SkinnableScoreCounter CreateScoreCounter() => new SkinnableScoreCounter();
protected SkinnableComboCounter CreateComboCounter() => new SkinnableComboCounter();
protected SkinnableHealthDisplay CreateHealthDisplay() => new SkinnableHealthDisplay();
protected virtual FailingLayer CreateFailingLayer() => new FailingLayer
{ {
ShowHealth = { BindTarget = ShowHealthbar } ShowHealth = { BindTarget = ShowHealthbar }
}; };
protected virtual KeyCounterDisplay CreateKeyCounter() => new KeyCounterDisplay protected KeyCounterDisplay CreateKeyCounter() => new KeyCounterDisplay
{ {
Anchor = Anchor.BottomRight, Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight, Origin = Anchor.BottomRight,
}; };
protected virtual SongProgress CreateProgress() => new SongProgress protected SongProgress CreateProgress() => new SongProgress
{ {
Anchor = Anchor.BottomLeft, Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft, Origin = Anchor.BottomLeft,
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
}; };
protected virtual HoldForMenuButton CreateHoldForMenuButton() => new HoldForMenuButton protected HoldForMenuButton CreateHoldForMenuButton() => new HoldForMenuButton
{ {
Anchor = Anchor.BottomRight, Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight, Origin = Anchor.BottomRight,
}; };
protected virtual ModDisplay CreateModsContainer() => new ModDisplay protected ModDisplay CreateModsContainer() => new ModDisplay
{ {
Anchor = Anchor.TopRight, Anchor = Anchor.TopRight,
Origin = Anchor.TopRight, Origin = Anchor.TopRight,
AutoSizeAxes = Axes.Both, AutoSizeAxes = Axes.Both,
}; };
protected virtual HitErrorDisplay CreateHitErrorDisplayOverlay() => new HitErrorDisplay(drawableRuleset?.FirstAvailableHitWindows); protected PlayerSettingsOverlay CreatePlayerSettingsOverlay() => new PlayerSettingsOverlay();
protected virtual PlayerSettingsOverlay CreatePlayerSettingsOverlay() => new PlayerSettingsOverlay();
public bool OnPressed(GlobalAction action) public bool OnPressed(GlobalAction action)
{ {

View File

@ -14,7 +14,6 @@ using osu.Framework.Timing;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Skinning;
namespace osu.Game.Screens.Play namespace osu.Game.Screens.Play
{ {
@ -181,12 +180,12 @@ namespace osu.Game.Screens.Play
info.TransformTo(nameof(info.Margin), new MarginPadding { Bottom = finalMargin }, transition_duration, Easing.In); info.TransformTo(nameof(info.Margin), new MarginPadding { Bottom = finalMargin }, transition_duration, Easing.In);
} }
public class SongProgressDisplay : Container, ISkinnableComponent public class SongProgressDisplay : Container
{ {
public SongProgressDisplay() public SongProgressDisplay()
{ {
// TODO: move actual implementation into this. // TODO: move actual implementation into this.
// exists for skin customisation purposes. // exists for skin customisation purposes (interface should be added to this container).
Masking = true; Masking = true;
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;

View File

@ -87,9 +87,9 @@ namespace osu.Game.Screens
private static Color4 getColourFor(object type) private static Color4 getColourFor(object type)
{ {
int hash = type.GetHashCode(); int hash = type.GetHashCode();
byte r = (byte)Math.Clamp(((hash & 0xFF0000) >> 16) * 0.8f, 20, 255); byte r = (byte)Math.Clamp(((hash & 0xFF0000) >> 16) * 2, 128, 255);
byte g = (byte)Math.Clamp(((hash & 0x00FF00) >> 8) * 0.8f, 20, 255); byte g = (byte)Math.Clamp(((hash & 0x00FF00) >> 8) * 2, 128, 255);
byte b = (byte)Math.Clamp((hash & 0x0000FF) * 0.8f, 20, 255); byte b = (byte)Math.Clamp((hash & 0x0000FF) * 2, 128, 255);
return new Color4(r, g, b, 255); return new Color4(r, g, b, 255);
} }
@ -109,10 +109,10 @@ namespace osu.Game.Screens
private readonly Container boxContainer; private readonly Container boxContainer;
public UnderConstructionMessage(string name) public UnderConstructionMessage(string name, string description = "is not yet ready for use!")
{ {
RelativeSizeAxes = Axes.Both; AutoSizeAxes = Axes.Both;
Size = new Vector2(0.3f);
Anchor = Anchor.Centre; Anchor = Anchor.Centre;
Origin = Anchor.Centre; Origin = Anchor.Centre;
@ -124,7 +124,7 @@ namespace osu.Game.Screens
{ {
CornerRadius = 20, CornerRadius = 20,
Masking = true, Masking = true,
RelativeSizeAxes = Axes.Both, AutoSizeAxes = Axes.Both,
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Children = new Drawable[] Children = new Drawable[]
@ -133,15 +133,15 @@ namespace osu.Game.Screens
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Colour = colour, Colour = colour.Darken(0.8f),
Alpha = 0.2f, Alpha = 0.8f,
Blending = BlendingParameters.Additive,
}, },
TextContainer = new FillFlowContainer TextContainer = new FillFlowContainer
{ {
AutoSizeAxes = Axes.Both, AutoSizeAxes = Axes.Both,
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Padding = new MarginPadding(20),
Direction = FillDirection.Vertical, Direction = FillDirection.Vertical,
Children = new Drawable[] Children = new Drawable[]
{ {
@ -157,14 +157,14 @@ namespace osu.Game.Screens
Anchor = Anchor.TopCentre, Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre, Origin = Anchor.TopCentre,
Text = name, Text = name,
Colour = colour.Lighten(0.8f), Colour = colour,
Font = OsuFont.GetFont(size: 36), Font = OsuFont.GetFont(size: 36),
}, },
new OsuSpriteText new OsuSpriteText
{ {
Anchor = Anchor.TopCentre, Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre, Origin = Anchor.TopCentre,
Text = "is not yet ready for use!", Text = description,
Font = OsuFont.GetFont(size: 20), Font = OsuFont.GetFont(size: 20),
}, },
new OsuSpriteText new OsuSpriteText

View File

@ -2,6 +2,7 @@
// 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.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Framework.Audio.Sample; using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
@ -9,7 +10,10 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Extensions;
using osu.Game.IO; using osu.Game.IO;
using osu.Game.Screens.Play.HUD;
using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
namespace osu.Game.Skinning namespace osu.Game.Skinning
@ -23,17 +27,77 @@ namespace osu.Game.Skinning
[UsedImplicitly(ImplicitUseKindFlags.InstantiatedWithFixedConstructorSignature)] [UsedImplicitly(ImplicitUseKindFlags.InstantiatedWithFixedConstructorSignature)]
public DefaultSkin(SkinInfo skin, IStorageResourceProvider resources) public DefaultSkin(SkinInfo skin, IStorageResourceProvider resources)
: base(skin) : base(skin, resources)
{ {
Configuration = new DefaultSkinConfiguration(); Configuration = new DefaultSkinConfiguration();
} }
public override Drawable GetDrawableComponent(ISkinComponent component) => null;
public override Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => null; public override Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => null;
public override ISample GetSample(ISampleInfo sampleInfo) => null; public override ISample GetSample(ISampleInfo sampleInfo) => null;
public override Drawable GetDrawableComponent(ISkinComponent component)
{
if (base.GetDrawableComponent(component) is Drawable c)
return c;
switch (component)
{
case SkinnableTargetComponent target:
switch (target.Target)
{
case SkinnableTarget.MainHUDComponents:
var skinnableTargetWrapper = new SkinnableTargetComponentsContainer(container =>
{
var score = container.OfType<DefaultScoreCounter>().FirstOrDefault();
var accuracy = container.OfType<DefaultAccuracyCounter>().FirstOrDefault();
var combo = container.OfType<DefaultComboCounter>().FirstOrDefault();
if (score != null)
{
score.Anchor = Anchor.TopCentre;
score.Origin = Anchor.TopCentre;
// elements default to beneath the health bar
const float vertical_offset = 30;
const float horizontal_padding = 20;
score.Position = new Vector2(0, vertical_offset);
if (accuracy != null)
{
accuracy.Position = new Vector2(-accuracy.ScreenSpaceDeltaToParentSpace(score.ScreenSpaceDrawQuad.Size).X / 2 - horizontal_padding, vertical_offset + 5);
accuracy.Origin = Anchor.TopRight;
accuracy.Anchor = Anchor.TopCentre;
}
if (combo != null)
{
combo.Position = new Vector2(accuracy.ScreenSpaceDeltaToParentSpace(score.ScreenSpaceDrawQuad.Size).X / 2 + horizontal_padding, vertical_offset + 5);
combo.Anchor = Anchor.TopCentre;
}
}
})
{
Children = new Drawable[]
{
new DefaultComboCounter(),
new DefaultScoreCounter(),
new DefaultAccuracyCounter(),
new DefaultHealthDisplay(),
}
};
return skinnableTargetWrapper;
}
break;
}
return null;
}
public override IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) public override IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup)
{ {
switch (lookup) switch (lookup)

View File

@ -13,7 +13,7 @@ using osuTK;
namespace osu.Game.Skinning.Editor namespace osu.Game.Skinning.Editor
{ {
public class SkinBlueprint : SelectionBlueprint<ISkinnableComponent> public class SkinBlueprint : SelectionBlueprint<ISkinnableDrawable>
{ {
private Container box; private Container box;
@ -26,7 +26,7 @@ namespace osu.Game.Skinning.Editor
protected override bool ShouldBeAlive => (drawable.IsAlive && Item.IsPresent) || (AlwaysShowWhenSelected && State == SelectionState.Selected); protected override bool ShouldBeAlive => (drawable.IsAlive && Item.IsPresent) || (AlwaysShowWhenSelected && State == SelectionState.Selected);
public SkinBlueprint(ISkinnableComponent component) public SkinBlueprint(ISkinnableDrawable component)
: base(component) : base(component)
{ {
} }

View File

@ -1,42 +1,97 @@
// 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.Collections.Generic;
using System.Collections.Specialized;
using System.Linq; using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Screens;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Screens;
using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Screens.Edit.Compose.Components;
namespace osu.Game.Skinning.Editor namespace osu.Game.Skinning.Editor
{ {
public class SkinBlueprintContainer : BlueprintContainer<ISkinnableComponent> public class SkinBlueprintContainer : BlueprintContainer<ISkinnableDrawable>
{ {
private readonly Drawable target; private readonly Drawable target;
private readonly List<BindableList<ISkinnableDrawable>> targetComponents = new List<BindableList<ISkinnableDrawable>>();
public SkinBlueprintContainer(Drawable target) public SkinBlueprintContainer(Drawable target)
{ {
this.target = target; this.target = target;
} }
[BackgroundDependencyLoader(true)]
private void load(SkinEditor editor)
{
SelectedItems.BindTo(editor.SelectedComponents);
}
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
checkForComponents(); // track each target container on the current screen.
} var targetContainers = target.ChildrenOfType<ISkinnableTarget>().ToArray();
private void checkForComponents() if (targetContainers.Length == 0)
{ {
foreach (var c in target.ChildrenOfType<ISkinnableComponent>().ToArray()) AddBlueprintFor(c); var targetScreen = target.ChildrenOfType<Screen>().LastOrDefault()?.GetType().Name ?? "this screen";
// We'd hope to eventually be running this in a more sensible way, but this handles situations where new drawables become present (ie. during ongoing gameplay) AddInternal(new ScreenWhiteBox.UnderConstructionMessage(targetScreen, "doesn't support skin customisation just yet."));
// or when drawables in the target are loaded asynchronously and may not be immediately available when this BlueprintContainer is loaded. return;
Scheduler.AddDelayed(checkForComponents, 1000);
} }
protected override SelectionHandler<ISkinnableComponent> CreateSelectionHandler() => new SkinSelectionHandler(); foreach (var targetContainer in targetContainers)
{
var bindableList = new BindableList<ISkinnableDrawable> { BindTarget = targetContainer.Components };
bindableList.BindCollectionChanged(componentsChanged, true);
protected override SelectionBlueprint<ISkinnableComponent> CreateBlueprintFor(ISkinnableComponent component) targetComponents.Add(bindableList);
}
}
private void componentsChanged(object sender, NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
foreach (var item in e.NewItems.Cast<ISkinnableDrawable>())
AddBlueprintFor(item);
break;
case NotifyCollectionChangedAction.Remove:
case NotifyCollectionChangedAction.Reset:
foreach (var item in e.OldItems.Cast<ISkinnableDrawable>())
RemoveBlueprintFor(item);
break;
case NotifyCollectionChangedAction.Replace:
foreach (var item in e.OldItems.Cast<ISkinnableDrawable>())
RemoveBlueprintFor(item);
foreach (var item in e.NewItems.Cast<ISkinnableDrawable>())
AddBlueprintFor(item);
break;
}
}
protected override void AddBlueprintFor(ISkinnableDrawable item)
{
if (!item.IsEditable)
return;
base.AddBlueprintFor(item);
}
protected override SelectionHandler<ISkinnableDrawable> CreateSelectionHandler() => new SkinSelectionHandler();
protected override SelectionBlueprint<ISkinnableDrawable> CreateBlueprintFor(ISkinnableDrawable component)
=> new SkinBlueprint(component); => new SkinBlueprint(component);
} }
} }

View File

@ -56,7 +56,7 @@ namespace osu.Game.Skinning.Editor
Spacing = new Vector2(20) Spacing = new Vector2(20)
}; };
var skinnableTypes = typeof(OsuGame).Assembly.GetTypes().Where(t => typeof(ISkinnableComponent).IsAssignableFrom(t)).ToArray(); var skinnableTypes = typeof(OsuGame).Assembly.GetTypes().Where(t => typeof(ISkinnableDrawable).IsAssignableFrom(t)).ToArray();
foreach (var type in skinnableTypes) foreach (var type in skinnableTypes)
{ {
@ -78,6 +78,9 @@ namespace osu.Game.Skinning.Editor
Debug.Assert(instance != null); Debug.Assert(instance != null);
if (!((ISkinnableDrawable)instance).IsEditable)
return null;
return new ToolboxComponentButton(instance); return new ToolboxComponentButton(instance);
} }
catch catch

View File

@ -4,6 +4,7 @@
using System; using System;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
@ -11,28 +12,41 @@ using osu.Framework.Testing;
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 osuTK;
namespace osu.Game.Skinning.Editor namespace osu.Game.Skinning.Editor
{ {
[Cached(typeof(SkinEditor))]
public class SkinEditor : FocusedOverlayContainer public class SkinEditor : FocusedOverlayContainer
{ {
public const double TRANSITION_DURATION = 500; public const double TRANSITION_DURATION = 500;
private readonly Drawable target; public readonly BindableList<ISkinnableDrawable> SelectedComponents = new BindableList<ISkinnableDrawable>();
private OsuTextFlowContainer headerText;
protected override bool StartHidden => true; protected override bool StartHidden => true;
public SkinEditor(Drawable target) private readonly Drawable targetScreen;
private OsuTextFlowContainer headerText;
private Bindable<Skin> currentSkin;
[Resolved]
private SkinManager skins { get; set; }
[Resolved]
private OsuColour colours { get; set; }
public SkinEditor(Drawable targetScreen)
{ {
this.target = target; this.targetScreen = targetScreen;
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours) private void load()
{ {
InternalChild = new OsuContextMenuContainer InternalChild = new OsuContextMenuContainer
{ {
@ -47,37 +61,145 @@ namespace osu.Game.Skinning.Editor
Origin = Anchor.TopCentre, Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.X RelativeSizeAxes = Axes.X
}, },
new SkinBlueprintContainer(target), new GridContainer
{
RelativeSizeAxes = Axes.Both,
ColumnDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize),
new Dimension()
},
Content = new[]
{
new Drawable[]
{
new SkinComponentToolbox(600) new SkinComponentToolbox(600)
{ {
Anchor = Anchor.CentreLeft, Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
RequestPlacement = placeComponent RequestPlacement = placeComponent
},
new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new SkinBlueprintContainer(targetScreen),
new FillFlowContainer
{
Direction = FillDirection.Horizontal,
AutoSizeAxes = Axes.Both,
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Spacing = new Vector2(5),
Padding = new MarginPadding
{
Top = 10,
Left = 10,
},
Margin = new MarginPadding
{
Right = 10,
Bottom = 10,
},
Children = new Drawable[]
{
new TriangleButton
{
Text = "Save Changes",
Width = 140,
Action = save,
},
new DangerousTriangleButton
{
Text = "Revert to default",
Width = 140,
Action = revert,
},
}
},
}
},
}
}
} }
} }
}; };
headerText.AddParagraph("Skin editor (preview)", cp => cp.Font = OsuFont.Default.With(size: 24));
headerText.AddParagraph("This is a preview of what is to come. Changes are lost on changing screens.", cp =>
{
cp.Font = OsuFont.Default.With(size: 12);
cp.Colour = colours.Yellow;
});
}
private void placeComponent(Type type)
{
var instance = (Drawable)Activator.CreateInstance(type);
var targetContainer = target.ChildrenOfType<IDefaultSkinnableTarget>().FirstOrDefault();
targetContainer?.Add(instance);
} }
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
Show(); Show();
// as long as the skin editor is loaded, let's make sure we can modify the current skin.
currentSkin = skins.CurrentSkin.GetBoundCopy();
// schedule ensures this only happens when the skin editor is visible.
// also avoid some weird endless recursion / bindable feedback loop (something to do with tracking skins across three different bindable types).
// probably something which will be factored out in a future database refactor so not too concerning for now.
currentSkin.BindValueChanged(skin => Scheduler.AddOnce(skinChanged), true);
}
private void skinChanged()
{
headerText.Clear();
headerText.AddParagraph("Skin editor", cp => cp.Font = OsuFont.Default.With(size: 24));
headerText.NewParagraph();
headerText.AddText("Currently editing ", cp =>
{
cp.Font = OsuFont.Default.With(size: 12);
cp.Colour = colours.Yellow;
});
headerText.AddText($"{currentSkin.Value.SkinInfo}", cp =>
{
cp.Font = OsuFont.Default.With(size: 12, weight: FontWeight.Bold);
cp.Colour = colours.Yellow;
});
skins.EnsureMutableSkin();
}
private void placeComponent(Type type)
{
if (!(Activator.CreateInstance(type) is ISkinnableDrawable component))
throw new InvalidOperationException($"Attempted to instantiate a component for placement which was not an {typeof(ISkinnableDrawable)}.");
getTarget(SkinnableTarget.MainHUDComponents)?.Add(component);
SelectedComponents.Clear();
SelectedComponents.Add(component);
}
private ISkinnableTarget getTarget(SkinnableTarget target)
{
return targetScreen.ChildrenOfType<ISkinnableTarget>().FirstOrDefault(c => c.Target == target);
}
private void revert()
{
SkinnableTargetContainer[] targetContainers = targetScreen.ChildrenOfType<SkinnableTargetContainer>().ToArray();
foreach (var t in targetContainers)
{
currentSkin.Value.ResetDrawableTarget(t);
// add back default components
getTarget(t.Target).Reload();
}
}
private void save()
{
SkinnableTargetContainer[] targetContainers = targetScreen.ChildrenOfType<SkinnableTargetContainer>().ToArray();
foreach (var t in targetContainers)
currentSkin.Value.UpdateDrawableTarget(t);
skins.Save(skins.CurrentSkin.Value);
} }
protected override bool OnHover(HoverEvent e) => true; protected override bool OnHover(HoverEvent e) => true;

View File

@ -71,7 +71,7 @@ namespace osu.Game.Skinning.Editor
target.RelativePositionAxes = Axes.Both; target.RelativePositionAxes = Axes.Both;
target.ScaleTo(VISIBLE_TARGET_SCALE, SkinEditor.TRANSITION_DURATION, Easing.OutQuint); target.ScaleTo(VISIBLE_TARGET_SCALE, SkinEditor.TRANSITION_DURATION, Easing.OutQuint);
target.MoveToX(0.1f, SkinEditor.TRANSITION_DURATION, Easing.OutQuint); target.MoveToX(0.095f, SkinEditor.TRANSITION_DURATION, Easing.OutQuint);
} }
else else
{ {

View File

@ -14,7 +14,7 @@ using osuTK;
namespace osu.Game.Skinning.Editor namespace osu.Game.Skinning.Editor
{ {
public class SkinSelectionHandler : SelectionHandler<ISkinnableComponent> public class SkinSelectionHandler : SelectionHandler<ISkinnableDrawable>
{ {
public override bool HandleRotation(float angle) public override bool HandleRotation(float angle)
{ {
@ -36,7 +36,7 @@ namespace osu.Game.Skinning.Editor
return true; return true;
} }
public override bool HandleMovement(MoveSelectionEvent<ISkinnableComponent> moveEvent) public override bool HandleMovement(MoveSelectionEvent<ISkinnableDrawable> moveEvent)
{ {
foreach (var c in SelectedBlueprints) foreach (var c in SelectedBlueprints)
{ {
@ -57,7 +57,7 @@ namespace osu.Game.Skinning.Editor
SelectionBox.CanReverse = false; SelectionBox.CanReverse = false;
} }
protected override void DeleteItems(IEnumerable<ISkinnableComponent> items) protected override void DeleteItems(IEnumerable<ISkinnableDrawable> items)
{ {
foreach (var i in items) foreach (var i in items)
{ {
@ -66,17 +66,22 @@ namespace osu.Game.Skinning.Editor
} }
} }
protected override IEnumerable<MenuItem> GetContextMenuItemsForSelection(IEnumerable<SelectionBlueprint<ISkinnableComponent>> selection) protected override IEnumerable<MenuItem> GetContextMenuItemsForSelection(IEnumerable<SelectionBlueprint<ISkinnableDrawable>> selection)
{ {
yield return new OsuMenuItem("Anchor") yield return new OsuMenuItem("Anchor")
{ {
Items = createAnchorItems().ToArray() Items = createAnchorItems(d => d.Anchor, applyAnchor).ToArray()
};
yield return new OsuMenuItem("Origin")
{
Items = createAnchorItems(d => d.Origin, applyOrigin).ToArray()
}; };
foreach (var item in base.GetContextMenuItemsForSelection(selection)) foreach (var item in base.GetContextMenuItemsForSelection(selection))
yield return item; yield return item;
IEnumerable<AnchorMenuItem> createAnchorItems() IEnumerable<AnchorMenuItem> createAnchorItems(Func<Drawable, Anchor> checkFunction, Action<Anchor> applyFunction)
{ {
var displayableAnchors = new[] var displayableAnchors = new[]
{ {
@ -93,14 +98,20 @@ namespace osu.Game.Skinning.Editor
return displayableAnchors.Select(a => return displayableAnchors.Select(a =>
{ {
return new AnchorMenuItem(a, selection, _ => applyAnchor(a)) return new AnchorMenuItem(a, selection, _ => applyFunction(a))
{ {
State = { Value = GetStateFromSelection(selection, c => ((Drawable)c.Item).Anchor == a) } State = { Value = GetStateFromSelection(selection, c => checkFunction((Drawable)c.Item) == a) }
}; };
}); });
} }
} }
private void applyOrigin(Anchor anchor)
{
foreach (var item in SelectedItems)
((Drawable)item).Origin = anchor;
}
private void applyAnchor(Anchor anchor) private void applyAnchor(Anchor anchor)
{ {
foreach (var item in SelectedItems) foreach (var item in SelectedItems)
@ -120,7 +131,7 @@ namespace osu.Game.Skinning.Editor
public class AnchorMenuItem : TernaryStateMenuItem public class AnchorMenuItem : TernaryStateMenuItem
{ {
public AnchorMenuItem(Anchor anchor, IEnumerable<SelectionBlueprint<ISkinnableComponent>> selection, Action<TernaryState> action) public AnchorMenuItem(Anchor anchor, IEnumerable<SelectionBlueprint<ISkinnableDrawable>> selection, Action<TernaryState> action)
: base(anchor.ToString(), getNextState, MenuItemType.Standard, action) : base(anchor.ToString(), getNextState, MenuItemType.Standard, action)
{ {
} }

View File

@ -8,7 +8,11 @@ namespace osu.Game.Skinning
/// <summary> /// <summary>
/// Denotes a drawable which, as a drawable, can be adjusted via skinning specifications. /// Denotes a drawable which, as a drawable, can be adjusted via skinning specifications.
/// </summary> /// </summary>
public interface ISkinnableComponent : IDrawable public interface ISkinnableDrawable : IDrawable
{ {
/// <summary>
/// Whether this component should be editable by an end user.
/// </summary>
bool IsEditable => true;
} }
} }

View File

@ -1,15 +1,44 @@
// 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.Collections.Generic;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Game.Extensions;
using osu.Game.Screens.Play.HUD;
namespace osu.Game.Skinning namespace osu.Game.Skinning
{ {
/// <summary> /// <summary>
/// Denotes a container which can house <see cref="ISkinnableComponent"/>s. /// Denotes a container which can house <see cref="ISkinnableDrawable"/>s.
/// </summary> /// </summary>
public interface ISkinnableTarget : IContainerCollection<Drawable> public interface ISkinnableTarget
{ {
/// <summary>
/// The definition of this target.
/// </summary>
SkinnableTarget Target { get; }
/// <summary>
/// A bindable list of components which are being tracked by this skinnable target.
/// </summary>
IBindableList<ISkinnableDrawable> Components { get; }
/// <summary>
/// Serialise all children as <see cref="SkinnableInfo"/>.
/// </summary>
/// <returns>The serialised content.</returns>
IEnumerable<SkinnableInfo> CreateSkinnableInfo() => Components.Select(d => ((Drawable)d).CreateSkinnableInfo());
/// <summary>
/// Reload this target from the current skin.
/// </summary>
void Reload();
/// <summary>
/// Add the provided item to this target.
/// </summary>
void Add(ISkinnableDrawable drawable);
} }
} }

View File

@ -10,39 +10,24 @@ using osuTK;
namespace osu.Game.Skinning namespace osu.Game.Skinning
{ {
public class LegacyAccuracyCounter : GameplayAccuracyCounter, ISkinnableComponent public class LegacyAccuracyCounter : GameplayAccuracyCounter, ISkinnableDrawable
{ {
private readonly ISkin skin; public LegacyAccuracyCounter()
public LegacyAccuracyCounter(ISkin skin)
{ {
Anchor = Anchor.TopRight; Anchor = Anchor.TopRight;
Origin = Anchor.TopRight; Origin = Anchor.TopRight;
Scale = new Vector2(0.6f); Scale = new Vector2(0.6f);
Margin = new MarginPadding(10); Margin = new MarginPadding(10);
this.skin = skin;
} }
[Resolved(canBeNull: true)] [Resolved(canBeNull: true)]
private HUDOverlay hud { get; set; } private HUDOverlay hud { get; set; }
protected sealed override OsuSpriteText CreateSpriteText() => new LegacySpriteText(skin, LegacyFont.Score) protected sealed override OsuSpriteText CreateSpriteText() => new LegacySpriteText(LegacyFont.Score)
{ {
Anchor = Anchor.TopRight, Anchor = Anchor.TopRight,
Origin = Anchor.TopRight, Origin = Anchor.TopRight,
}; };
protected override void Update()
{
base.Update();
if (hud?.ScoreCounter.Drawable is LegacyScoreCounter score)
{
// for now align with the score counter. eventually this will be user customisable.
Y = Parent.ToLocalSpace(score.ScreenSpaceDrawQuad.BottomRight).Y;
}
}
} }
} }

View File

@ -16,11 +16,13 @@ using osuTK.Graphics;
namespace osu.Game.Skinning namespace osu.Game.Skinning
{ {
public class LegacyHealthDisplay : HealthDisplay public class LegacyHealthDisplay : HealthDisplay, ISkinnableDrawable
{ {
private const double epic_cutoff = 0.5; private const double epic_cutoff = 0.5;
private readonly Skin skin; [Resolved]
private ISkinSource skin { get; set; }
private LegacyHealthPiece fill; private LegacyHealthPiece fill;
private LegacyHealthPiece marker; private LegacyHealthPiece marker;
@ -28,11 +30,6 @@ namespace osu.Game.Skinning
private bool isNewStyle; private bool isNewStyle;
public LegacyHealthDisplay(Skin skin)
{
this.skin = skin;
}
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
@ -79,7 +76,7 @@ namespace osu.Game.Skinning
protected override void Flash(JudgementResult result) => marker.Flash(result); protected override void Flash(JudgementResult result) => marker.Flash(result);
private static Texture getTexture(Skin skin, string name) => skin.GetTexture($"scorebar-{name}"); private static Texture getTexture(ISkinSource skin, string name) => skin.GetTexture($"scorebar-{name}");
private static Color4 getFillColour(double hp) private static Color4 getFillColour(double hp)
{ {
@ -98,7 +95,7 @@ namespace osu.Game.Skinning
private readonly Texture dangerTexture; private readonly Texture dangerTexture;
private readonly Texture superDangerTexture; private readonly Texture superDangerTexture;
public LegacyOldStyleMarker(Skin skin) public LegacyOldStyleMarker(ISkinSource skin)
{ {
normalTexture = getTexture(skin, "ki"); normalTexture = getTexture(skin, "ki");
dangerTexture = getTexture(skin, "kidanger"); dangerTexture = getTexture(skin, "kidanger");
@ -129,9 +126,9 @@ namespace osu.Game.Skinning
public class LegacyNewStyleMarker : LegacyMarker public class LegacyNewStyleMarker : LegacyMarker
{ {
private readonly Skin skin; private readonly ISkinSource skin;
public LegacyNewStyleMarker(Skin skin) public LegacyNewStyleMarker(ISkinSource skin)
{ {
this.skin = skin; this.skin = skin;
} }
@ -153,7 +150,7 @@ namespace osu.Game.Skinning
internal class LegacyOldStyleFill : LegacyHealthPiece internal class LegacyOldStyleFill : LegacyHealthPiece
{ {
public LegacyOldStyleFill(Skin skin) public LegacyOldStyleFill(ISkinSource skin)
{ {
// required for sizing correctly.. // required for sizing correctly..
var firstFrame = getTexture(skin, "colour-0"); var firstFrame = getTexture(skin, "colour-0");
@ -176,7 +173,7 @@ namespace osu.Game.Skinning
internal class LegacyNewStyleFill : LegacyHealthPiece internal class LegacyNewStyleFill : LegacyHealthPiece
{ {
public LegacyNewStyleFill(Skin skin) public LegacyNewStyleFill(ISkinSource skin)
{ {
InternalChild = new Sprite InternalChild = new Sprite
{ {

View File

@ -12,7 +12,6 @@ namespace osu.Game.Skinning
/// </summary> /// </summary>
public class LegacyRollingCounter : RollingCounter<int> public class LegacyRollingCounter : RollingCounter<int>
{ {
private readonly ISkin skin;
private readonly LegacyFont font; private readonly LegacyFont font;
protected override bool IsRollingProportional => true; protected override bool IsRollingProportional => true;
@ -20,11 +19,9 @@ namespace osu.Game.Skinning
/// <summary> /// <summary>
/// Creates a new <see cref="LegacyRollingCounter"/>. /// Creates a new <see cref="LegacyRollingCounter"/>.
/// </summary> /// </summary>
/// <param name="skin">The <see cref="ISkin"/> from which to get counter number sprites.</param>
/// <param name="font">The legacy font to use for the counter.</param> /// <param name="font">The legacy font to use for the counter.</param>
public LegacyRollingCounter(ISkin skin, LegacyFont font) public LegacyRollingCounter(LegacyFont font)
{ {
this.skin = skin;
this.font = font; this.font = font;
} }
@ -33,6 +30,6 @@ namespace osu.Game.Skinning
return Math.Abs(newValue - currentValue) * 75.0; return Math.Abs(newValue - currentValue) * 75.0;
} }
protected sealed override OsuSpriteText CreateSpriteText() => new LegacySpriteText(skin, font); protected sealed override OsuSpriteText CreateSpriteText() => new LegacySpriteText(font);
} }
} }

View File

@ -8,26 +8,22 @@ using osuTK;
namespace osu.Game.Skinning namespace osu.Game.Skinning
{ {
public class LegacyScoreCounter : GameplayScoreCounter, ISkinnableComponent public class LegacyScoreCounter : GameplayScoreCounter, ISkinnableDrawable
{ {
private readonly ISkin skin;
protected override double RollingDuration => 1000; protected override double RollingDuration => 1000;
protected override Easing RollingEasing => Easing.Out; protected override Easing RollingEasing => Easing.Out;
public LegacyScoreCounter(ISkin skin) public LegacyScoreCounter()
: base(6) : base(6)
{ {
Anchor = Anchor.TopRight; Anchor = Anchor.TopRight;
Origin = Anchor.TopRight; Origin = Anchor.TopRight;
this.skin = skin;
Scale = new Vector2(0.96f); Scale = new Vector2(0.96f);
Margin = new MarginPadding(10); Margin = new MarginPadding(10);
} }
protected sealed override OsuSpriteText CreateSpriteText() => new LegacySpriteText(skin, LegacyFont.Score) protected sealed override OsuSpriteText CreateSpriteText() => new LegacySpriteText(LegacyFont.Score)
{ {
Anchor = Anchor.TopRight, Anchor = Anchor.TopRight,
Origin = Anchor.TopRight, Origin = Anchor.TopRight,

View File

@ -59,7 +59,7 @@ namespace osu.Game.Skinning
} }
protected LegacySkin(SkinInfo skin, [CanBeNull] IResourceStore<byte[]> storage, [CanBeNull] IStorageResourceProvider resources, string filename) protected LegacySkin(SkinInfo skin, [CanBeNull] IResourceStore<byte[]> storage, [CanBeNull] IStorageResourceProvider resources, string filename)
: base(skin) : base(skin, resources)
{ {
using (var stream = storage?.GetStream(filename)) using (var stream = storage?.GetStream(filename))
{ {
@ -322,8 +322,42 @@ namespace osu.Game.Skinning
public override Drawable GetDrawableComponent(ISkinComponent component) public override Drawable GetDrawableComponent(ISkinComponent component)
{ {
if (base.GetDrawableComponent(component) is Drawable c)
return c;
switch (component) switch (component)
{ {
case SkinnableTargetComponent target:
switch (target.Target)
{
case SkinnableTarget.MainHUDComponents:
var skinnableTargetWrapper = new SkinnableTargetComponentsContainer(container =>
{
var score = container.OfType<LegacyScoreCounter>().FirstOrDefault();
var accuracy = container.OfType<GameplayAccuracyCounter>().FirstOrDefault();
if (score != null && accuracy != null)
{
accuracy.Y = container.ToLocalSpace(score.ScreenSpaceDrawQuad.BottomRight).Y;
}
})
{
Children = new[]
{
// TODO: these should fallback to the osu!classic skin.
GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.ComboCounter)) ?? new DefaultComboCounter(),
GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.ScoreCounter)) ?? new DefaultScoreCounter(),
GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.AccuracyCounter)) ?? new DefaultAccuracyCounter(),
GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.HealthDisplay)) ?? new DefaultHealthDisplay(),
}
};
return skinnableTargetWrapper;
}
return null;
case HUDSkinComponent hudComponent: case HUDSkinComponent hudComponent:
{ {
if (!this.HasFont(LegacyFont.Score)) if (!this.HasFont(LegacyFont.Score))
@ -335,13 +369,13 @@ namespace osu.Game.Skinning
return new LegacyComboCounter(); return new LegacyComboCounter();
case HUDSkinComponents.ScoreCounter: case HUDSkinComponents.ScoreCounter:
return new LegacyScoreCounter(this); return new LegacyScoreCounter();
case HUDSkinComponents.AccuracyCounter: case HUDSkinComponents.AccuracyCounter:
return new LegacyAccuracyCounter(this); return new LegacyAccuracyCounter();
case HUDSkinComponents.HealthDisplay: case HUDSkinComponents.HealthDisplay:
return new LegacyHealthDisplay(this); return new LegacyHealthDisplay();
} }
return null; return null;

View File

@ -2,6 +2,7 @@
// 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.Threading.Tasks; using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Text; using osu.Framework.Text;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
@ -9,19 +10,26 @@ using osuTK;
namespace osu.Game.Skinning namespace osu.Game.Skinning
{ {
public class LegacySpriteText : OsuSpriteText public sealed class LegacySpriteText : OsuSpriteText
{ {
private readonly LegacyGlyphStore glyphStore; private readonly LegacyFont font;
private LegacyGlyphStore glyphStore;
protected override char FixedWidthReferenceCharacter => '5'; protected override char FixedWidthReferenceCharacter => '5';
protected override char[] FixedWidthExcludeCharacters => new[] { ',', '.', '%', 'x' }; protected override char[] FixedWidthExcludeCharacters => new[] { ',', '.', '%', 'x' };
public LegacySpriteText(ISkin skin, LegacyFont font) public LegacySpriteText(LegacyFont font)
{ {
this.font = font;
Shadow = false; Shadow = false;
UseFullGlyphHeight = false; UseFullGlyphHeight = false;
}
[BackgroundDependencyLoader]
private void load(ISkinSource skin)
{
Font = new FontUsage(skin.GetFontPrefix(font), 1, fixedWidth: true); Font = new FontUsage(skin.GetFontPrefix(font), 1, fixedWidth: true);
Spacing = new Vector2(-skin.GetFontOverlap(font), 0); Spacing = new Vector2(-skin.GetFontOverlap(font), 0);

View File

@ -2,12 +2,18 @@
// 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 System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Newtonsoft.Json;
using osu.Framework.Audio.Sample; using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.IO;
using osu.Game.Screens.Play.HUD;
namespace osu.Game.Skinning namespace osu.Game.Skinning
{ {
@ -17,7 +23,9 @@ namespace osu.Game.Skinning
public SkinConfiguration Configuration { get; protected set; } public SkinConfiguration Configuration { get; protected set; }
public abstract Drawable GetDrawableComponent(ISkinComponent componentName); public IDictionary<SkinnableTarget, SkinnableInfo[]> DrawableComponentInfo => drawableComponentInfo;
private readonly Dictionary<SkinnableTarget, SkinnableInfo[]> drawableComponentInfo = new Dictionary<SkinnableTarget, SkinnableInfo[]>();
public abstract ISample GetSample(ISampleInfo sampleInfo); public abstract ISample GetSample(ISampleInfo sampleInfo);
@ -27,9 +35,65 @@ namespace osu.Game.Skinning
public abstract IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup); public abstract IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup);
protected Skin(SkinInfo skin) protected Skin(SkinInfo skin, IStorageResourceProvider resources)
{ {
SkinInfo = skin; SkinInfo = skin;
// we may want to move this to some kind of async operation in the future.
foreach (SkinnableTarget skinnableTarget in Enum.GetValues(typeof(SkinnableTarget)))
{
string filename = $"{skinnableTarget}.json";
// skininfo files may be null for default skin.
var fileInfo = SkinInfo.Files?.FirstOrDefault(f => f.Filename == filename);
if (fileInfo == null)
continue;
var bytes = resources?.Files.Get(fileInfo.FileInfo.StoragePath);
if (bytes == null)
continue;
string jsonContent = Encoding.UTF8.GetString(bytes);
DrawableComponentInfo[skinnableTarget] = JsonConvert.DeserializeObject<IEnumerable<SkinnableInfo>>(jsonContent).ToArray();
}
}
/// <summary>
/// Remove all stored customisations for the provided target.
/// </summary>
/// <param name="targetContainer">The target container to reset.</param>
public void ResetDrawableTarget(ISkinnableTarget targetContainer)
{
DrawableComponentInfo.Remove(targetContainer.Target);
}
/// <summary>
/// Update serialised information for the provided target.
/// </summary>
/// <param name="targetContainer">The target container to serialise to this skin.</param>
public void UpdateDrawableTarget(ISkinnableTarget targetContainer)
{
DrawableComponentInfo[targetContainer.Target] = targetContainer.CreateSkinnableInfo().ToArray();
}
public virtual Drawable GetDrawableComponent(ISkinComponent component)
{
switch (component)
{
case SkinnableTargetComponent target:
if (!DrawableComponentInfo.TryGetValue(target.Target, out var skinnableInfo))
return null;
return new SkinnableTargetComponentsContainer
{
ChildrenEnumerable = skinnableInfo.Select(i => i.CreateInstance())
};
}
return null;
} }
#region Disposal #region Disposal

View File

@ -6,9 +6,11 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Audio.Sample; using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
@ -152,6 +154,48 @@ namespace osu.Game.Skinning
/// <returns>A <see cref="Skin"/> instance correlating to the provided <see cref="SkinInfo"/>.</returns> /// <returns>A <see cref="Skin"/> instance correlating to the provided <see cref="SkinInfo"/>.</returns>
public Skin GetSkin(SkinInfo skinInfo) => skinInfo.CreateInstance(legacyDefaultResources, this); public Skin GetSkin(SkinInfo skinInfo) => skinInfo.CreateInstance(legacyDefaultResources, this);
/// <summary>
/// Ensure that the current skin is in a state it can accept user modifications.
/// This will create a copy of any internal skin and being tracking in the database if not already.
/// </summary>
public void EnsureMutableSkin()
{
if (CurrentSkinInfo.Value.ID >= 1) return;
var skin = CurrentSkin.Value;
// if the user is attempting to save one of the default skin implementations, create a copy first.
CurrentSkinInfo.Value = Import(new SkinInfo
{
Name = skin.SkinInfo.Name + " (modified)",
Creator = skin.SkinInfo.Creator,
InstantiationInfo = skin.SkinInfo.InstantiationInfo,
}).Result;
}
public void Save(Skin skin)
{
if (skin.SkinInfo.ID <= 0)
throw new InvalidOperationException($"Attempting to save a skin which is not yet tracked. Call {nameof(EnsureMutableSkin)} first.");
foreach (var drawableInfo in skin.DrawableComponentInfo)
{
string json = JsonConvert.SerializeObject(drawableInfo.Value, new JsonSerializerSettings { Formatting = Formatting.Indented });
using (var streamContent = new MemoryStream(Encoding.UTF8.GetBytes(json)))
{
string filename = $"{drawableInfo.Key}.json";
var oldFile = skin.SkinInfo.Files.FirstOrDefault(f => f.Filename == filename);
if (oldFile != null)
ReplaceFile(skin.SkinInfo, oldFile, streamContent, oldFile.Filename);
else
AddFile(skin.SkinInfo, streamContent, filename);
}
}
}
/// <summary> /// <summary>
/// Perform a lookup query on available <see cref="SkinInfo"/>s. /// Perform a lookup query on available <see cref="SkinInfo"/>s.
/// </summary> /// </summary>

View File

@ -3,10 +3,8 @@
namespace osu.Game.Skinning namespace osu.Game.Skinning
{ {
/// <summary> public enum SkinnableTarget
/// The default placement location for new <see cref="ISkinnableComponent"/>s.
/// </summary>
public interface IDefaultSkinnableTarget : ISkinnableTarget
{ {
MainHUDComponents
} }
} }

View File

@ -0,0 +1,17 @@
// 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.
namespace osu.Game.Skinning
{
public class SkinnableTargetComponent : ISkinComponent
{
public readonly SkinnableTarget Target;
public string LookupName => Target.ToString();
public SkinnableTargetComponent(SkinnableTarget target)
{
Target = target;
}
}
}

View File

@ -0,0 +1,46 @@
// 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 Newtonsoft.Json;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
namespace osu.Game.Skinning
{
/// <summary>
/// A container which groups the components of a <see cref="SkinnableTargetContainer"/> into a single object.
/// Optionally also applies a default layout to the components.
/// </summary>
[Serializable]
public class SkinnableTargetComponentsContainer : Container, ISkinnableDrawable
{
public bool IsEditable => false;
private readonly Action<Container> applyDefaults;
/// <summary>
/// Construct a wrapper with defaults that should be applied once.
/// </summary>
/// <param name="applyDefaults">A function to apply the default layout.</param>
public SkinnableTargetComponentsContainer(Action<Container> applyDefaults)
: this()
{
this.applyDefaults = applyDefaults;
}
[JsonConstructor]
public SkinnableTargetComponentsContainer()
{
RelativeSizeAxes = Axes.Both;
}
protected override void LoadComplete()
{
base.LoadComplete();
// schedule is required to allow children to run their LoadComplete and take on their correct sizes.
ScheduleAfterChildren(() => applyDefaults?.Invoke(this));
}
}
}

View File

@ -0,0 +1,71 @@
// 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.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
namespace osu.Game.Skinning
{
public class SkinnableTargetContainer : SkinReloadableDrawable, ISkinnableTarget
{
private SkinnableTargetComponentsContainer content;
public SkinnableTarget Target { get; }
public IBindableList<ISkinnableDrawable> Components => components;
private readonly BindableList<ISkinnableDrawable> components = new BindableList<ISkinnableDrawable>();
public SkinnableTargetContainer(SkinnableTarget target)
{
Target = target;
}
/// <summary>
/// Reload all components in this container from the current skin.
/// </summary>
public void Reload()
{
ClearInternal();
components.Clear();
content = CurrentSkin.GetDrawableComponent(new SkinnableTargetComponent(Target)) as SkinnableTargetComponentsContainer;
if (content != null)
{
LoadComponentAsync(content, wrapper =>
{
AddInternal(wrapper);
components.AddRange(wrapper.Children.OfType<ISkinnableDrawable>());
});
}
}
/// <summary>
/// Add a new skinnable component to this target.
/// </summary>
/// <param name="component">The component to add.</param>
/// <exception cref="NotSupportedException">Thrown when attempting to add an element to a target which is not supported by the current skin.</exception>
/// <exception cref="ArgumentException">Thrown if the provided instance is not a <see cref="Drawable"/>.</exception>
public void Add(ISkinnableDrawable component)
{
if (content == null)
throw new NotSupportedException("Attempting to add a new component to a target container which is not supported by the current skin.");
if (!(component is Drawable drawable))
throw new ArgumentException($"Provided argument must be of type {nameof(Drawable)}.", nameof(drawable));
content.Add(drawable);
components.Add(component);
}
protected override void SkinChanged(ISkinSource skin, bool allowFallback)
{
base.SkinChanged(skin, allowFallback);
Reload();
}
}
}