mirror of
https://github.com/osukey/osukey.git
synced 2025-08-05 07:33:55 +09:00
Merge branch 'refactor-combo-colour-retrieval' into legacy-beatmap-combo-offset
This commit is contained in:
@ -1,7 +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 JetBrains.Annotations;
|
||||
using osu.Framework.IO.Stores;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.IO;
|
||||
using osuTK.Graphics;
|
||||
|
||||
@ -9,8 +11,14 @@ namespace osu.Game.Skinning
|
||||
{
|
||||
public class DefaultLegacySkin : LegacySkin
|
||||
{
|
||||
public DefaultLegacySkin(IResourceStore<byte[]> storage, IStorageResourceProvider resources)
|
||||
: base(Info, storage, resources, string.Empty)
|
||||
public DefaultLegacySkin(IStorageResourceProvider resources)
|
||||
: this(Info, resources)
|
||||
{
|
||||
}
|
||||
|
||||
[UsedImplicitly(ImplicitUseKindFlags.InstantiatedWithFixedConstructorSignature)]
|
||||
public DefaultLegacySkin(SkinInfo skin, IStorageResourceProvider resources)
|
||||
: base(skin, new NamespacedResourceStore<byte[]>(resources.Resources, "Skins/Legacy"), resources, string.Empty)
|
||||
{
|
||||
Configuration.CustomColours["SliderBall"] = new Color4(2, 170, 255, 255);
|
||||
Configuration.AddComboColours(
|
||||
@ -27,7 +35,8 @@ namespace osu.Game.Skinning
|
||||
{
|
||||
ID = SkinInfo.CLASSIC_SKIN, // this is temporary until database storage is decided upon.
|
||||
Name = "osu!classic",
|
||||
Creator = "team osu!"
|
||||
Creator = "team osu!",
|
||||
InstantiationInfo = typeof(DefaultLegacySkin).GetInvariantInstantiationInfo()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,8 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
@ -9,23 +11,129 @@ using osu.Framework.Graphics.OpenGL.Textures;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps.Formats;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.IO;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osu.Game.Screens.Play.HUD.HitErrorMeters;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Skinning
|
||||
{
|
||||
public class DefaultSkin : Skin
|
||||
{
|
||||
public DefaultSkin()
|
||||
: base(SkinInfo.Default)
|
||||
private readonly IStorageResourceProvider resources;
|
||||
|
||||
public DefaultSkin(IStorageResourceProvider resources)
|
||||
: this(SkinInfo.Default, resources)
|
||||
{
|
||||
}
|
||||
|
||||
[UsedImplicitly(ImplicitUseKindFlags.InstantiatedWithFixedConstructorSignature)]
|
||||
public DefaultSkin(SkinInfo skin, IStorageResourceProvider resources)
|
||||
: base(skin, resources)
|
||||
{
|
||||
this.resources = resources;
|
||||
Configuration = new DefaultSkinConfiguration();
|
||||
}
|
||||
|
||||
public override Drawable GetDrawableComponent(ISkinComponent component) => 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)
|
||||
{
|
||||
foreach (var lookup in sampleInfo.LookupNames)
|
||||
{
|
||||
var sample = resources.AudioManager.Samples.Get(lookup);
|
||||
if (sample != null)
|
||||
return sample;
|
||||
}
|
||||
|
||||
return 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;
|
||||
}
|
||||
|
||||
var hitError = container.OfType<HitErrorMeter>().FirstOrDefault();
|
||||
|
||||
if (hitError != null)
|
||||
{
|
||||
hitError.Anchor = Anchor.CentreLeft;
|
||||
hitError.Origin = Anchor.CentreLeft;
|
||||
}
|
||||
|
||||
var hitError2 = container.OfType<HitErrorMeter>().LastOrDefault();
|
||||
|
||||
if (hitError2 != null)
|
||||
{
|
||||
hitError2.Anchor = Anchor.CentreRight;
|
||||
hitError2.Scale = new Vector2(-1, 1);
|
||||
// origin flipped to match scale above.
|
||||
hitError2.Origin = Anchor.CentreLeft;
|
||||
}
|
||||
}
|
||||
})
|
||||
{
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new DefaultComboCounter(),
|
||||
new DefaultScoreCounter(),
|
||||
new DefaultAccuracyCounter(),
|
||||
new DefaultHealthDisplay(),
|
||||
new SongProgress(),
|
||||
new BarHitErrorMeter(),
|
||||
new BarHitErrorMeter(),
|
||||
}
|
||||
};
|
||||
|
||||
return skinnableTargetWrapper;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public override IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup)
|
||||
{
|
||||
|
185
osu.Game/Skinning/Editor/SkinBlueprint.cs
Normal file
185
osu.Game/Skinning/Editor/SkinBlueprint.cs
Normal file
@ -0,0 +1,185 @@
|
||||
// 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.Allocation;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Skinning.Editor
|
||||
{
|
||||
public class SkinBlueprint : SelectionBlueprint<ISkinnableDrawable>
|
||||
{
|
||||
private Container box;
|
||||
|
||||
private Container outlineBox;
|
||||
|
||||
private AnchorOriginVisualiser anchorOriginVisualiser;
|
||||
|
||||
private Drawable drawable => (Drawable)Item;
|
||||
|
||||
protected override bool ShouldBeAlive => drawable.IsAlive && Item.IsPresent;
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; }
|
||||
|
||||
public SkinBlueprint(ISkinnableDrawable component)
|
||||
: base(component)
|
||||
{
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
box = new Container
|
||||
{
|
||||
Children = new Drawable[]
|
||||
{
|
||||
outlineBox = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
BorderThickness = 3,
|
||||
BorderColour = Color4.White,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0f,
|
||||
AlwaysPresent = true,
|
||||
},
|
||||
}
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Text = Item.GetType().Name,
|
||||
Font = OsuFont.Default.With(size: 10, weight: FontWeight.Bold),
|
||||
Anchor = Anchor.BottomRight,
|
||||
Origin = Anchor.TopRight,
|
||||
},
|
||||
},
|
||||
},
|
||||
anchorOriginVisualiser = new AnchorOriginVisualiser(drawable)
|
||||
{
|
||||
Alpha = 0,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
updateSelectedState();
|
||||
this.FadeInFromZero(200, Easing.OutQuint);
|
||||
}
|
||||
|
||||
protected override void OnSelected()
|
||||
{
|
||||
// base logic hides selected blueprints when not selected, but skin blueprints don't do that.
|
||||
updateSelectedState();
|
||||
}
|
||||
|
||||
protected override void OnDeselected()
|
||||
{
|
||||
// base logic hides selected blueprints when not selected, but skin blueprints don't do that.
|
||||
updateSelectedState();
|
||||
}
|
||||
|
||||
private void updateSelectedState()
|
||||
{
|
||||
outlineBox.FadeColour(colours.Pink.Opacity(IsSelected ? 1 : 0.5f), 200, Easing.OutQuint);
|
||||
outlineBox.Child.FadeTo(IsSelected ? 0.2f : 0, 200, Easing.OutQuint);
|
||||
|
||||
anchorOriginVisualiser.FadeTo(IsSelected ? 1 : 0, 200, Easing.OutQuint);
|
||||
}
|
||||
|
||||
private Quad drawableQuad;
|
||||
|
||||
public override Quad ScreenSpaceDrawQuad => drawableQuad;
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
drawableQuad = drawable.ScreenSpaceDrawQuad;
|
||||
var quad = ToLocalSpace(drawable.ScreenSpaceDrawQuad);
|
||||
|
||||
box.Position = drawable.ToSpaceOfOtherDrawable(Vector2.Zero, this);
|
||||
box.Size = quad.Size;
|
||||
box.Rotation = drawable.Rotation;
|
||||
box.Scale = new Vector2(MathF.Sign(drawable.Scale.X), MathF.Sign(drawable.Scale.Y));
|
||||
}
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => drawable.ReceivePositionalInputAt(screenSpacePos);
|
||||
|
||||
public override Vector2 ScreenSpaceSelectionPoint => drawable.ToScreenSpace(drawable.OriginPosition);
|
||||
|
||||
public override Quad SelectionQuad => drawable.ScreenSpaceDrawQuad;
|
||||
}
|
||||
|
||||
internal class AnchorOriginVisualiser : CompositeDrawable
|
||||
{
|
||||
private readonly Drawable drawable;
|
||||
|
||||
private readonly Box originBox;
|
||||
|
||||
private readonly Box anchorBox;
|
||||
private readonly Box anchorLine;
|
||||
|
||||
public AnchorOriginVisualiser(Drawable drawable)
|
||||
{
|
||||
this.drawable = drawable;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
anchorLine = new Box
|
||||
{
|
||||
Colour = Color4.Yellow,
|
||||
Height = 2,
|
||||
},
|
||||
originBox = new Box
|
||||
{
|
||||
Colour = Color4.Red,
|
||||
Origin = Anchor.Centre,
|
||||
Size = new Vector2(5),
|
||||
},
|
||||
anchorBox = new Box
|
||||
{
|
||||
Colour = Color4.Red,
|
||||
Origin = Anchor.Centre,
|
||||
Size = new Vector2(5),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (drawable.Parent == null)
|
||||
return;
|
||||
|
||||
originBox.Position = drawable.ToSpaceOfOtherDrawable(drawable.OriginPosition, this);
|
||||
anchorBox.Position = drawable.Parent.ToSpaceOfOtherDrawable(drawable.AnchorPosition, this);
|
||||
|
||||
var point1 = ToLocalSpace(anchorBox.ScreenSpaceDrawQuad.Centre);
|
||||
var point2 = ToLocalSpace(originBox.ScreenSpaceDrawQuad.Centre);
|
||||
|
||||
anchorLine.Position = point1;
|
||||
anchorLine.Width = (point2 - point1).Length;
|
||||
anchorLine.Rotation = MathHelper.RadiansToDegrees(MathF.Atan2(point2.Y - point1.Y, point2.X - point1.X));
|
||||
}
|
||||
}
|
||||
}
|
97
osu.Game/Skinning/Editor/SkinBlueprintContainer.cs
Normal file
97
osu.Game/Skinning/Editor/SkinBlueprintContainer.cs
Normal file
@ -0,0 +1,97 @@
|
||||
// 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.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Screens;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
|
||||
namespace osu.Game.Skinning.Editor
|
||||
{
|
||||
public class SkinBlueprintContainer : BlueprintContainer<ISkinnableDrawable>
|
||||
{
|
||||
private readonly Drawable target;
|
||||
|
||||
private readonly List<BindableList<ISkinnableDrawable>> targetComponents = new List<BindableList<ISkinnableDrawable>>();
|
||||
|
||||
public SkinBlueprintContainer(Drawable target)
|
||||
{
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader(true)]
|
||||
private void load(SkinEditor editor)
|
||||
{
|
||||
SelectedItems.BindTo(editor.SelectedComponents);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
// track each target container on the current screen.
|
||||
var targetContainers = target.ChildrenOfType<ISkinnableTarget>().ToArray();
|
||||
|
||||
if (targetContainers.Length == 0)
|
||||
{
|
||||
var targetScreen = target.ChildrenOfType<Screen>().LastOrDefault()?.GetType().Name ?? "this screen";
|
||||
|
||||
AddInternal(new ScreenWhiteBox.UnderConstructionMessage(targetScreen, "doesn't support skin customisation just yet."));
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var targetContainer in targetContainers)
|
||||
{
|
||||
var bindableList = new BindableList<ISkinnableDrawable> { BindTarget = targetContainer.Components };
|
||||
bindableList.BindCollectionChanged(componentsChanged, true);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
173
osu.Game/Skinning/Editor/SkinComponentToolbox.cs
Normal file
173
osu.Game/Skinning/Editor/SkinComponentToolbox.cs
Normal file
@ -0,0 +1,173 @@
|
||||
// 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.Diagnostics;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Effects;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Skinning.Editor
|
||||
{
|
||||
public class SkinComponentToolbox : ScrollingToolboxGroup
|
||||
{
|
||||
public Action<Type> RequestPlacement;
|
||||
|
||||
private const float component_display_scale = 0.8f;
|
||||
|
||||
[Cached]
|
||||
private ScoreProcessor scoreProcessor = new ScoreProcessor
|
||||
{
|
||||
Combo = { Value = RNG.Next(1, 1000) },
|
||||
TotalScore = { Value = RNG.Next(1000, 10000000) }
|
||||
};
|
||||
|
||||
[Cached(typeof(HealthProcessor))]
|
||||
private HealthProcessor healthProcessor = new DrainingHealthProcessor(0);
|
||||
|
||||
public SkinComponentToolbox(float height)
|
||||
: base("Components", height)
|
||||
{
|
||||
RelativeSizeAxes = Axes.None;
|
||||
Width = 200;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
FillFlowContainer fill;
|
||||
|
||||
Child = fill = new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(20)
|
||||
};
|
||||
|
||||
var skinnableTypes = typeof(OsuGame).Assembly.GetTypes()
|
||||
.Where(t => !t.IsInterface)
|
||||
.Where(t => typeof(ISkinnableDrawable).IsAssignableFrom(t))
|
||||
.ToArray();
|
||||
|
||||
foreach (var type in skinnableTypes)
|
||||
{
|
||||
var component = attemptAddComponent(type);
|
||||
|
||||
if (component != null)
|
||||
{
|
||||
component.RequestPlacement = t => RequestPlacement?.Invoke(t);
|
||||
fill.Add(component);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static ToolboxComponentButton attemptAddComponent(Type type)
|
||||
{
|
||||
try
|
||||
{
|
||||
var instance = (Drawable)Activator.CreateInstance(type);
|
||||
|
||||
Debug.Assert(instance != null);
|
||||
|
||||
if (!((ISkinnableDrawable)instance).IsEditable)
|
||||
return null;
|
||||
|
||||
return new ToolboxComponentButton(instance);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private class ToolboxComponentButton : OsuButton
|
||||
{
|
||||
protected override bool ShouldBeConsideredForInput(Drawable child) => false;
|
||||
|
||||
public override bool PropagateNonPositionalInputSubTree => false;
|
||||
|
||||
private readonly Drawable component;
|
||||
|
||||
public Action<Type> RequestPlacement;
|
||||
|
||||
private Container innerContainer;
|
||||
|
||||
public ToolboxComponentButton(Drawable component)
|
||||
{
|
||||
this.component = component;
|
||||
|
||||
Enabled.Value = true;
|
||||
|
||||
RelativeSizeAxes = Axes.X;
|
||||
Height = 70;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
BackgroundColour = colours.Gray3;
|
||||
Content.EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Type = EdgeEffectType.Shadow,
|
||||
Radius = 2,
|
||||
Offset = new Vector2(0, 1),
|
||||
Colour = Color4.Black.Opacity(0.5f)
|
||||
};
|
||||
|
||||
AddRange(new Drawable[]
|
||||
{
|
||||
new OsuSpriteText
|
||||
{
|
||||
Text = component.GetType().Name,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
},
|
||||
innerContainer = new Container
|
||||
{
|
||||
Y = 10,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Scale = new Vector2(component_display_scale),
|
||||
Masking = true,
|
||||
Child = component
|
||||
}
|
||||
});
|
||||
|
||||
// adjust provided component to fit / display in a known state.
|
||||
component.Anchor = Anchor.Centre;
|
||||
component.Origin = Anchor.Centre;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
if (component.RelativeSizeAxes != Axes.None)
|
||||
{
|
||||
innerContainer.AutoSizeAxes = Axes.None;
|
||||
innerContainer.Height = 100;
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool OnClick(ClickEvent e)
|
||||
{
|
||||
RequestPlacement?.Invoke(component.GetType());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
250
osu.Game/Skinning/Editor/SkinEditor.cs
Normal file
250
osu.Game/Skinning/Editor/SkinEditor.cs
Normal file
@ -0,0 +1,250 @@
|
||||
// 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 osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Cursor;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Skinning.Editor
|
||||
{
|
||||
[Cached(typeof(SkinEditor))]
|
||||
public class SkinEditor : VisibilityContainer
|
||||
{
|
||||
public const double TRANSITION_DURATION = 500;
|
||||
|
||||
public readonly BindableList<ISkinnableDrawable> SelectedComponents = new BindableList<ISkinnableDrawable>();
|
||||
|
||||
protected override bool StartHidden => true;
|
||||
|
||||
private readonly Drawable targetScreen;
|
||||
|
||||
private OsuTextFlowContainer headerText;
|
||||
|
||||
private Bindable<Skin> currentSkin;
|
||||
|
||||
[Resolved]
|
||||
private SkinManager skins { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; }
|
||||
|
||||
private bool hasBegunMutating;
|
||||
|
||||
public SkinEditor(Drawable targetScreen)
|
||||
{
|
||||
this.targetScreen = targetScreen;
|
||||
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
InternalChild = new OsuContextMenuContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
headerText = new OsuTextFlowContainer
|
||||
{
|
||||
TextAnchor = Anchor.TopCentre,
|
||||
Padding = new MarginPadding(20),
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
RelativeSizeAxes = Axes.X
|
||||
},
|
||||
new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
ColumnDimensions = new[]
|
||||
{
|
||||
new Dimension(GridSizeMode.AutoSize),
|
||||
new Dimension()
|
||||
},
|
||||
Content = new[]
|
||||
{
|
||||
new Drawable[]
|
||||
{
|
||||
new SkinComponentToolbox(600)
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
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,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
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 =>
|
||||
{
|
||||
hasBegunMutating = false;
|
||||
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();
|
||||
hasBegunMutating = true;
|
||||
}
|
||||
|
||||
private void placeComponent(Type type)
|
||||
{
|
||||
var targetContainer = getTarget(SkinnableTarget.MainHUDComponents);
|
||||
|
||||
if (targetContainer == null)
|
||||
return;
|
||||
|
||||
if (!(Activator.CreateInstance(type) is ISkinnableDrawable component))
|
||||
throw new InvalidOperationException($"Attempted to instantiate a component for placement which was not an {typeof(ISkinnableDrawable)}.");
|
||||
|
||||
var drawableComponent = (Drawable)component;
|
||||
|
||||
// give newly added components a sane starting location.
|
||||
drawableComponent.Origin = Anchor.TopCentre;
|
||||
drawableComponent.Anchor = Anchor.TopCentre;
|
||||
drawableComponent.Y = targetContainer.DrawSize.Y / 2;
|
||||
|
||||
targetContainer.Add(component);
|
||||
|
||||
SelectedComponents.Clear();
|
||||
SelectedComponents.Add(component);
|
||||
}
|
||||
|
||||
private IEnumerable<ISkinnableTarget> availableTargets => targetScreen.ChildrenOfType<ISkinnableTarget>();
|
||||
|
||||
private ISkinnableTarget getTarget(SkinnableTarget target)
|
||||
{
|
||||
return availableTargets.FirstOrDefault(c => c.Target == target);
|
||||
}
|
||||
|
||||
private void revert()
|
||||
{
|
||||
ISkinnableTarget[] targetContainers = availableTargets.ToArray();
|
||||
|
||||
foreach (var t in targetContainers)
|
||||
{
|
||||
currentSkin.Value.ResetDrawableTarget(t);
|
||||
|
||||
// add back default components
|
||||
getTarget(t.Target).Reload();
|
||||
}
|
||||
}
|
||||
|
||||
public void Save()
|
||||
{
|
||||
if (!hasBegunMutating)
|
||||
return;
|
||||
|
||||
ISkinnableTarget[] targetContainers = availableTargets.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 OnMouseDown(MouseDownEvent e) => true;
|
||||
|
||||
protected override void PopIn()
|
||||
{
|
||||
this.FadeIn(TRANSITION_DURATION, Easing.OutQuint);
|
||||
}
|
||||
|
||||
protected override void PopOut()
|
||||
{
|
||||
this.FadeOut(TRANSITION_DURATION, Easing.OutQuint);
|
||||
}
|
||||
|
||||
public void DeleteItems(ISkinnableDrawable[] items)
|
||||
{
|
||||
foreach (var item in items)
|
||||
availableTargets.FirstOrDefault(t => t.Components.Contains(item))?.Remove(item);
|
||||
}
|
||||
}
|
||||
}
|
99
osu.Game/Skinning/Editor/SkinEditorOverlay.cs
Normal file
99
osu.Game/Skinning/Editor/SkinEditorOverlay.cs
Normal file
@ -0,0 +1,99 @@
|
||||
// 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;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Input.Bindings;
|
||||
|
||||
namespace osu.Game.Skinning.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// A container which handles loading a skin editor on user request for a specified target.
|
||||
/// This also handles the scaling / positioning adjustment of the target.
|
||||
/// </summary>
|
||||
public class SkinEditorOverlay : CompositeDrawable, IKeyBindingHandler<GlobalAction>
|
||||
{
|
||||
private readonly ScalingContainer target;
|
||||
private SkinEditor skinEditor;
|
||||
|
||||
public const float VISIBLE_TARGET_SCALE = 0.8f;
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; }
|
||||
|
||||
public SkinEditorOverlay(ScalingContainer target)
|
||||
{
|
||||
this.target = target;
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
}
|
||||
|
||||
public bool OnPressed(GlobalAction action)
|
||||
{
|
||||
switch (action)
|
||||
{
|
||||
case GlobalAction.Back:
|
||||
if (skinEditor?.State.Value == Visibility.Visible)
|
||||
{
|
||||
skinEditor.ToggleVisibility();
|
||||
return true;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case GlobalAction.ToggleSkinEditor:
|
||||
if (skinEditor == null)
|
||||
{
|
||||
LoadComponentAsync(skinEditor = new SkinEditor(target), AddInternal);
|
||||
skinEditor.State.BindValueChanged(editorVisibilityChanged);
|
||||
}
|
||||
else
|
||||
skinEditor.ToggleVisibility();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void editorVisibilityChanged(ValueChangedEvent<Visibility> visibility)
|
||||
{
|
||||
if (visibility.NewValue == Visibility.Visible)
|
||||
{
|
||||
target.Masking = true;
|
||||
target.AllowScaling = false;
|
||||
target.RelativePositionAxes = Axes.Both;
|
||||
|
||||
target.ScaleTo(VISIBLE_TARGET_SCALE, SkinEditor.TRANSITION_DURATION, Easing.OutQuint);
|
||||
target.MoveToX(0.095f, SkinEditor.TRANSITION_DURATION, Easing.OutQuint);
|
||||
}
|
||||
else
|
||||
{
|
||||
target.AllowScaling = true;
|
||||
|
||||
target.ScaleTo(1, SkinEditor.TRANSITION_DURATION, Easing.OutQuint).OnComplete(_ => target.Masking = false);
|
||||
target.MoveToX(0f, SkinEditor.TRANSITION_DURATION, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
|
||||
public void OnReleased(GlobalAction action)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exit any existing skin editor due to the game state changing.
|
||||
/// </summary>
|
||||
public void Reset()
|
||||
{
|
||||
skinEditor?.Save();
|
||||
skinEditor?.Hide();
|
||||
skinEditor?.Expire();
|
||||
|
||||
skinEditor = null;
|
||||
}
|
||||
}
|
||||
}
|
347
osu.Game/Skinning/Editor/SkinSelectionHandler.cs
Normal file
347
osu.Game/Skinning/Editor/SkinSelectionHandler.cs
Normal file
@ -0,0 +1,347 @@
|
||||
// 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 osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.EnumExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Skinning.Editor
|
||||
{
|
||||
public class SkinSelectionHandler : SelectionHandler<ISkinnableDrawable>
|
||||
{
|
||||
[Resolved]
|
||||
private SkinEditor skinEditor { get; set; }
|
||||
|
||||
public override bool HandleRotation(float angle)
|
||||
{
|
||||
if (SelectedBlueprints.Count == 1)
|
||||
{
|
||||
// for single items, rotate around the origin rather than the selection centre.
|
||||
((Drawable)SelectedBlueprints.First().Item).Rotation += angle;
|
||||
}
|
||||
else
|
||||
{
|
||||
var selectionQuad = getSelectionQuad();
|
||||
|
||||
foreach (var b in SelectedBlueprints)
|
||||
{
|
||||
var drawableItem = (Drawable)b.Item;
|
||||
|
||||
var rotatedPosition = RotatePointAroundOrigin(b.ScreenSpaceSelectionPoint, selectionQuad.Centre, angle);
|
||||
updateDrawablePosition(drawableItem, rotatedPosition);
|
||||
|
||||
drawableItem.Rotation += angle;
|
||||
}
|
||||
}
|
||||
|
||||
// this isn't always the case but let's be lenient for now.
|
||||
return true;
|
||||
}
|
||||
|
||||
public override bool HandleScale(Vector2 scale, Anchor anchor)
|
||||
{
|
||||
// convert scale to screen space
|
||||
scale = ToScreenSpace(scale) - ToScreenSpace(Vector2.Zero);
|
||||
|
||||
adjustScaleFromAnchor(ref scale, anchor);
|
||||
|
||||
// the selection quad is always upright, so use an AABB rect to make mutating the values easier.
|
||||
var selectionRect = getSelectionQuad().AABBFloat;
|
||||
|
||||
// If the selection has no area we cannot scale it
|
||||
if (selectionRect.Area == 0)
|
||||
return false;
|
||||
|
||||
// copy to mutate, as we will need to compare to the original later on.
|
||||
var adjustedRect = selectionRect;
|
||||
|
||||
// first, remove any scale axis we are not interested in.
|
||||
if (anchor.HasFlagFast(Anchor.x1)) scale.X = 0;
|
||||
if (anchor.HasFlagFast(Anchor.y1)) scale.Y = 0;
|
||||
|
||||
bool shouldAspectLock =
|
||||
// for now aspect lock scale adjustments that occur at corners..
|
||||
(!anchor.HasFlagFast(Anchor.x1) && !anchor.HasFlagFast(Anchor.y1))
|
||||
// ..or if any of the selection have been rotated.
|
||||
// this is to avoid requiring skew logic (which would likely not be the user's expected transform anyway).
|
||||
|| SelectedBlueprints.Any(b => !Precision.AlmostEquals(((Drawable)b.Item).Rotation, 0));
|
||||
|
||||
if (shouldAspectLock)
|
||||
{
|
||||
if (anchor.HasFlagFast(Anchor.x1))
|
||||
// if dragging from the horizontal centre, only a vertical component is available.
|
||||
scale.X = scale.Y / selectionRect.Height * selectionRect.Width;
|
||||
else
|
||||
// in all other cases (arbitrarily) use the horizontal component for aspect lock.
|
||||
scale.Y = scale.X / selectionRect.Width * selectionRect.Height;
|
||||
}
|
||||
|
||||
if (anchor.HasFlagFast(Anchor.x0)) adjustedRect.X -= scale.X;
|
||||
if (anchor.HasFlagFast(Anchor.y0)) adjustedRect.Y -= scale.Y;
|
||||
|
||||
adjustedRect.Width += scale.X;
|
||||
adjustedRect.Height += scale.Y;
|
||||
|
||||
// scale adjust applied to each individual item should match that of the quad itself.
|
||||
var scaledDelta = new Vector2(
|
||||
MathF.Max(adjustedRect.Width / selectionRect.Width, 0),
|
||||
MathF.Max(adjustedRect.Height / selectionRect.Height, 0)
|
||||
);
|
||||
|
||||
foreach (var b in SelectedBlueprints)
|
||||
{
|
||||
var drawableItem = (Drawable)b.Item;
|
||||
|
||||
// each drawable's relative position should be maintained in the scaled quad.
|
||||
var screenPosition = b.ScreenSpaceSelectionPoint;
|
||||
|
||||
var relativePositionInOriginal =
|
||||
new Vector2(
|
||||
(screenPosition.X - selectionRect.TopLeft.X) / selectionRect.Width,
|
||||
(screenPosition.Y - selectionRect.TopLeft.Y) / selectionRect.Height
|
||||
);
|
||||
|
||||
var newPositionInAdjusted = new Vector2(
|
||||
adjustedRect.TopLeft.X + adjustedRect.Width * relativePositionInOriginal.X,
|
||||
adjustedRect.TopLeft.Y + adjustedRect.Height * relativePositionInOriginal.Y
|
||||
);
|
||||
|
||||
updateDrawablePosition(drawableItem, newPositionInAdjusted);
|
||||
drawableItem.Scale *= scaledDelta;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public override bool HandleFlip(Direction direction)
|
||||
{
|
||||
var selectionQuad = getSelectionQuad();
|
||||
Vector2 scaleFactor = direction == Direction.Horizontal ? new Vector2(-1, 1) : new Vector2(1, -1);
|
||||
|
||||
foreach (var b in SelectedBlueprints)
|
||||
{
|
||||
var drawableItem = (Drawable)b.Item;
|
||||
|
||||
var flippedPosition = GetFlippedPosition(direction, selectionQuad, b.ScreenSpaceSelectionPoint);
|
||||
|
||||
updateDrawablePosition(drawableItem, flippedPosition);
|
||||
|
||||
drawableItem.Scale *= scaleFactor;
|
||||
drawableItem.Rotation -= drawableItem.Rotation % 180 * 2;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public override bool HandleMovement(MoveSelectionEvent<ISkinnableDrawable> moveEvent)
|
||||
{
|
||||
foreach (var c in SelectedBlueprints)
|
||||
{
|
||||
var item = c.Item;
|
||||
Drawable drawable = (Drawable)item;
|
||||
|
||||
drawable.Position += drawable.ScreenSpaceDeltaToParentSpace(moveEvent.ScreenSpaceDelta);
|
||||
|
||||
if (item.UsesFixedAnchor) continue;
|
||||
|
||||
applyClosestAnchor(drawable);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void applyClosestAnchor(Drawable drawable) => applyAnchor(drawable, getClosestAnchor(drawable));
|
||||
|
||||
protected override void OnSelectionChanged()
|
||||
{
|
||||
base.OnSelectionChanged();
|
||||
|
||||
SelectionBox.CanRotate = true;
|
||||
SelectionBox.CanScaleX = true;
|
||||
SelectionBox.CanScaleY = true;
|
||||
SelectionBox.CanReverse = false;
|
||||
}
|
||||
|
||||
protected override void DeleteItems(IEnumerable<ISkinnableDrawable> items) =>
|
||||
skinEditor.DeleteItems(items.ToArray());
|
||||
|
||||
protected override IEnumerable<MenuItem> GetContextMenuItemsForSelection(IEnumerable<SelectionBlueprint<ISkinnableDrawable>> selection)
|
||||
{
|
||||
var closestItem = new TernaryStateRadioMenuItem("Closest", MenuItemType.Standard, _ => applyClosestAnchors())
|
||||
{
|
||||
State = { Value = GetStateFromSelection(selection, c => !c.Item.UsesFixedAnchor) }
|
||||
};
|
||||
|
||||
yield return new OsuMenuItem("Anchor")
|
||||
{
|
||||
Items = createAnchorItems((d, a) => d.UsesFixedAnchor && ((Drawable)d).Anchor == a, applyFixedAnchors)
|
||||
.Prepend(closestItem)
|
||||
.ToArray()
|
||||
};
|
||||
|
||||
yield return new OsuMenuItem("Origin")
|
||||
{
|
||||
Items = createAnchorItems((d, o) => ((Drawable)d).Origin == o, applyOrigins).ToArray()
|
||||
};
|
||||
|
||||
foreach (var item in base.GetContextMenuItemsForSelection(selection))
|
||||
yield return item;
|
||||
|
||||
IEnumerable<TernaryStateMenuItem> createAnchorItems(Func<ISkinnableDrawable, Anchor, bool> checkFunction, Action<Anchor> applyFunction)
|
||||
{
|
||||
var displayableAnchors = new[]
|
||||
{
|
||||
Anchor.TopLeft,
|
||||
Anchor.TopCentre,
|
||||
Anchor.TopRight,
|
||||
Anchor.CentreLeft,
|
||||
Anchor.Centre,
|
||||
Anchor.CentreRight,
|
||||
Anchor.BottomLeft,
|
||||
Anchor.BottomCentre,
|
||||
Anchor.BottomRight,
|
||||
};
|
||||
return displayableAnchors.Select(a =>
|
||||
{
|
||||
return new TernaryStateRadioMenuItem(a.ToString(), MenuItemType.Standard, _ => applyFunction(a))
|
||||
{
|
||||
State = { Value = GetStateFromSelection(selection, c => checkFunction(c.Item, a)) }
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static void updateDrawablePosition(Drawable drawable, Vector2 screenSpacePosition)
|
||||
{
|
||||
drawable.Position =
|
||||
drawable.Parent.ToLocalSpace(screenSpacePosition) - drawable.AnchorPosition;
|
||||
}
|
||||
|
||||
private void applyOrigins(Anchor origin)
|
||||
{
|
||||
foreach (var item in SelectedItems)
|
||||
{
|
||||
var drawable = (Drawable)item;
|
||||
|
||||
if (origin == drawable.Origin) continue;
|
||||
|
||||
var previousOrigin = drawable.OriginPosition;
|
||||
drawable.Origin = origin;
|
||||
drawable.Position += drawable.OriginPosition - previousOrigin;
|
||||
|
||||
if (item.UsesFixedAnchor) continue;
|
||||
|
||||
applyClosestAnchor(drawable);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A screen-space quad surrounding all selected drawables, accounting for their full displayed size.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private Quad getSelectionQuad() =>
|
||||
GetSurroundingQuad(SelectedBlueprints.SelectMany(b => b.Item.ScreenSpaceDrawQuad.GetVertices().ToArray()));
|
||||
|
||||
private void applyFixedAnchors(Anchor anchor)
|
||||
{
|
||||
foreach (var item in SelectedItems)
|
||||
{
|
||||
var drawable = (Drawable)item;
|
||||
|
||||
item.UsesFixedAnchor = true;
|
||||
applyAnchor(drawable, anchor);
|
||||
}
|
||||
}
|
||||
|
||||
private void applyClosestAnchors()
|
||||
{
|
||||
foreach (var item in SelectedItems)
|
||||
{
|
||||
item.UsesFixedAnchor = false;
|
||||
applyClosestAnchor((Drawable)item);
|
||||
}
|
||||
}
|
||||
|
||||
private static Anchor getClosestAnchor(Drawable drawable)
|
||||
{
|
||||
var parent = drawable.Parent;
|
||||
|
||||
if (parent == null)
|
||||
return drawable.Anchor;
|
||||
|
||||
var screenPosition = getScreenPosition();
|
||||
|
||||
var absolutePosition = parent.ToLocalSpace(screenPosition);
|
||||
var factor = parent.RelativeToAbsoluteFactor;
|
||||
|
||||
var result = default(Anchor);
|
||||
|
||||
static Anchor getAnchorFromPosition(float xOrY, Anchor anchor0, Anchor anchor1, Anchor anchor2)
|
||||
{
|
||||
if (xOrY >= 2 / 3f)
|
||||
return anchor2;
|
||||
|
||||
if (xOrY >= 1 / 3f)
|
||||
return anchor1;
|
||||
|
||||
return anchor0;
|
||||
}
|
||||
|
||||
result |= getAnchorFromPosition(absolutePosition.X / factor.X, Anchor.x0, Anchor.x1, Anchor.x2);
|
||||
result |= getAnchorFromPosition(absolutePosition.Y / factor.Y, Anchor.y0, Anchor.y1, Anchor.y2);
|
||||
|
||||
return result;
|
||||
|
||||
Vector2 getScreenPosition()
|
||||
{
|
||||
var quad = drawable.ScreenSpaceDrawQuad;
|
||||
var origin = drawable.Origin;
|
||||
|
||||
var pos = quad.TopLeft;
|
||||
|
||||
if (origin.HasFlagFast(Anchor.x2))
|
||||
pos.X += quad.Width;
|
||||
else if (origin.HasFlagFast(Anchor.x1))
|
||||
pos.X += quad.Width / 2f;
|
||||
|
||||
if (origin.HasFlagFast(Anchor.y2))
|
||||
pos.Y += quad.Height;
|
||||
else if (origin.HasFlagFast(Anchor.y1))
|
||||
pos.Y += quad.Height / 2f;
|
||||
|
||||
return pos;
|
||||
}
|
||||
}
|
||||
|
||||
private static void applyAnchor(Drawable drawable, Anchor anchor)
|
||||
{
|
||||
if (anchor == drawable.Anchor) return;
|
||||
|
||||
var previousAnchor = drawable.AnchorPosition;
|
||||
drawable.Anchor = anchor;
|
||||
drawable.Position -= drawable.AnchorPosition - previousAnchor;
|
||||
}
|
||||
|
||||
private static void adjustScaleFromAnchor(ref Vector2 scale, Anchor reference)
|
||||
{
|
||||
// cancel out scale in axes we don't care about (based on which drag handle was used).
|
||||
if ((reference & Anchor.x1) > 0) scale.X = 0;
|
||||
if ((reference & Anchor.y1) > 0) scale.Y = 0;
|
||||
|
||||
// reverse the scale direction if dragging from top or left.
|
||||
if ((reference & Anchor.x0) > 0) scale.X = -scale.X;
|
||||
if ((reference & Anchor.y0) > 0) scale.Y = -scale.Y;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
// 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.Linq;
|
||||
|
||||
namespace osu.Game.Skinning
|
||||
{
|
||||
public class HUDSkinComponent : ISkinComponent
|
||||
{
|
||||
public readonly HUDSkinComponents Component;
|
||||
|
||||
public HUDSkinComponent(HUDSkinComponents component)
|
||||
{
|
||||
Component = component;
|
||||
}
|
||||
|
||||
protected virtual string ComponentName => Component.ToString();
|
||||
|
||||
public string LookupName =>
|
||||
string.Join('/', new[] { "HUD", ComponentName }.Where(s => !string.IsNullOrEmpty(s)));
|
||||
}
|
||||
}
|
@ -9,5 +9,8 @@ namespace osu.Game.Skinning
|
||||
ScoreCounter,
|
||||
AccuracyCounter,
|
||||
HealthDisplay,
|
||||
SongProgress,
|
||||
BarHitErrorMeter,
|
||||
ColourHitErrorMeter,
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,8 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace osu.Game.Skinning
|
||||
{
|
||||
@ -11,5 +13,18 @@ namespace osu.Game.Skinning
|
||||
public interface ISkinSource : ISkin
|
||||
{
|
||||
event Action SourceChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Find the first (if any) skin that can fulfill the lookup.
|
||||
/// This should be used for cases where subsequent lookups (for related components) need to occur on the same skin.
|
||||
/// </summary>
|
||||
/// <returns>The skin to be used for subsequent lookups, or <c>null</c> if none is available.</returns>
|
||||
[CanBeNull]
|
||||
ISkin FindProvider(Func<ISkin, bool> lookupFunction);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve all sources available for lookup, with highest priority source first.
|
||||
/// </summary>
|
||||
IEnumerable<ISkin> AllSources { get; }
|
||||
}
|
||||
}
|
||||
|
25
osu.Game/Skinning/ISkinnableDrawable.cs
Normal file
25
osu.Game/Skinning/ISkinnableDrawable.cs
Normal file
@ -0,0 +1,25 @@
|
||||
// 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;
|
||||
|
||||
namespace osu.Game.Skinning
|
||||
{
|
||||
/// <summary>
|
||||
/// Denotes a drawable which, as a drawable, can be adjusted via skinning specifications.
|
||||
/// </summary>
|
||||
public interface ISkinnableDrawable : IDrawable
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether this component should be editable by an end user.
|
||||
/// </summary>
|
||||
bool IsEditable => true;
|
||||
|
||||
/// <summary>
|
||||
/// In the context of the skin layout editor, whether this <see cref="ISkinnableDrawable"/> has a permanent anchor defined.
|
||||
/// If <see langword="false"/>, this <see cref="ISkinnableDrawable"/>'s <see cref="Drawable.Anchor"/> is automatically determined by proximity,
|
||||
/// If <see langword="true"/>, a fixed anchor point has been defined.
|
||||
/// </summary>
|
||||
bool UsesFixedAnchor { get; set; }
|
||||
}
|
||||
}
|
51
osu.Game/Skinning/ISkinnableTarget.cs
Normal file
51
osu.Game/Skinning/ISkinnableTarget.cs
Normal file
@ -0,0 +1,51 @@
|
||||
// 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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
|
||||
namespace osu.Game.Skinning
|
||||
{
|
||||
/// <summary>
|
||||
/// Denotes a container which can house <see cref="ISkinnableDrawable"/>s.
|
||||
/// </summary>
|
||||
public interface ISkinnableTarget : IDrawable
|
||||
{
|
||||
/// <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 a new skinnable component to this target.
|
||||
/// </summary>
|
||||
/// <param name="drawable">The component to add.</param>
|
||||
void Add(ISkinnableDrawable drawable);
|
||||
|
||||
/// <summary>
|
||||
/// Remove an existing skinnable component from this target.
|
||||
/// </summary>
|
||||
/// <param name="component">The component to remove.</param>
|
||||
public void Remove(ISkinnableDrawable component);
|
||||
}
|
||||
}
|
@ -4,46 +4,32 @@
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Skinning
|
||||
{
|
||||
public class LegacyAccuracyCounter : PercentageCounter, IAccuracyCounter
|
||||
public class LegacyAccuracyCounter : GameplayAccuracyCounter, ISkinnableDrawable
|
||||
{
|
||||
private readonly ISkin skin;
|
||||
public bool UsesFixedAnchor { get; set; }
|
||||
|
||||
public LegacyAccuracyCounter(ISkin skin)
|
||||
public LegacyAccuracyCounter()
|
||||
{
|
||||
Anchor = Anchor.TopRight;
|
||||
Origin = Anchor.TopRight;
|
||||
|
||||
Scale = new Vector2(0.6f);
|
||||
Margin = new MarginPadding(10);
|
||||
|
||||
this.skin = skin;
|
||||
}
|
||||
|
||||
[Resolved(canBeNull: true)]
|
||||
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,
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.IO.Stores;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps;
|
||||
@ -26,6 +27,25 @@ namespace osu.Game.Skinning
|
||||
Configuration.AllowDefaultComboColoursFallback = false;
|
||||
}
|
||||
|
||||
public override Drawable GetDrawableComponent(ISkinComponent component)
|
||||
{
|
||||
if (component is SkinnableTargetComponent targetComponent)
|
||||
{
|
||||
switch (targetComponent.Target)
|
||||
{
|
||||
case SkinnableTarget.MainHUDComponents:
|
||||
// this should exist in LegacySkin instead, but there isn't a fallback skin for LegacySkins yet.
|
||||
// therefore keep the check here until fallback default legacy skin is supported.
|
||||
if (!this.HasFont(LegacyFont.Score))
|
||||
return null;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return base.GetDrawableComponent(component);
|
||||
}
|
||||
|
||||
public override IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup)
|
||||
{
|
||||
switch (lookup)
|
||||
@ -62,6 +82,6 @@ namespace osu.Game.Skinning
|
||||
}
|
||||
|
||||
private static SkinInfo createSkinInfo(BeatmapInfo beatmap) =>
|
||||
new SkinInfo { Name = beatmap.ToString(), Creator = beatmap.Metadata.Author.ToString() };
|
||||
new SkinInfo { Name = beatmap.ToString(), Creator = beatmap.Metadata?.AuthorString };
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ using osuTK.Graphics;
|
||||
namespace osu.Game.Skinning
|
||||
{
|
||||
/// <summary>
|
||||
/// Compatibility methods to convert osu!stable colours to osu!lazer-compatible ones. Should be used for legacy skins only.
|
||||
/// Compatibility methods to apply osu!stable quirks to colours. Should be used for legacy skins only.
|
||||
/// </summary>
|
||||
public static class LegacyColourCompatibility
|
||||
{
|
||||
|
@ -16,11 +16,10 @@ using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Skinning
|
||||
{
|
||||
public class LegacyHealthDisplay : CompositeDrawable, IHealthDisplay
|
||||
public class LegacyHealthDisplay : HealthDisplay, ISkinnableDrawable
|
||||
{
|
||||
private const double epic_cutoff = 0.5;
|
||||
|
||||
private readonly Skin skin;
|
||||
private LegacyHealthPiece fill;
|
||||
private LegacyHealthPiece marker;
|
||||
|
||||
@ -28,22 +27,16 @@ namespace osu.Game.Skinning
|
||||
|
||||
private bool isNewStyle;
|
||||
|
||||
public Bindable<double> Current { get; } = new BindableDouble(1)
|
||||
{
|
||||
MinValue = 0,
|
||||
MaxValue = 1
|
||||
};
|
||||
|
||||
public LegacyHealthDisplay(Skin skin)
|
||||
{
|
||||
this.skin = skin;
|
||||
}
|
||||
public bool UsesFixedAnchor { get; set; }
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
private void load(ISkinSource source)
|
||||
{
|
||||
AutoSizeAxes = Axes.Both;
|
||||
|
||||
var skin = source.FindProvider(s => getTexture(s, "bg") != null);
|
||||
|
||||
// the marker lookup to decide which display style must be performed on the source of the bg, which is the most common element.
|
||||
isNewStyle = getTexture(skin, "marker") != null;
|
||||
|
||||
// background implementation is the same for both versions.
|
||||
@ -83,9 +76,9 @@ namespace osu.Game.Skinning
|
||||
marker.Position = fill.Position + new Vector2(fill.DrawWidth, isNewStyle ? fill.DrawHeight / 2 : 0);
|
||||
}
|
||||
|
||||
public 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(ISkin skin, string name) => skin?.GetTexture($"scorebar-{name}");
|
||||
|
||||
private static Color4 getFillColour(double hp)
|
||||
{
|
||||
@ -104,7 +97,7 @@ namespace osu.Game.Skinning
|
||||
private readonly Texture dangerTexture;
|
||||
private readonly Texture superDangerTexture;
|
||||
|
||||
public LegacyOldStyleMarker(Skin skin)
|
||||
public LegacyOldStyleMarker(ISkin skin)
|
||||
{
|
||||
normalTexture = getTexture(skin, "ki");
|
||||
dangerTexture = getTexture(skin, "kidanger");
|
||||
@ -135,9 +128,9 @@ namespace osu.Game.Skinning
|
||||
|
||||
public class LegacyNewStyleMarker : LegacyMarker
|
||||
{
|
||||
private readonly Skin skin;
|
||||
private readonly ISkin skin;
|
||||
|
||||
public LegacyNewStyleMarker(Skin skin)
|
||||
public LegacyNewStyleMarker(ISkin skin)
|
||||
{
|
||||
this.skin = skin;
|
||||
}
|
||||
@ -157,9 +150,9 @@ namespace osu.Game.Skinning
|
||||
}
|
||||
}
|
||||
|
||||
internal class LegacyOldStyleFill : LegacyHealthPiece
|
||||
internal abstract class LegacyFill : LegacyHealthPiece
|
||||
{
|
||||
public LegacyOldStyleFill(Skin skin)
|
||||
protected LegacyFill(ISkin skin)
|
||||
{
|
||||
// required for sizing correctly..
|
||||
var firstFrame = getTexture(skin, "colour-0");
|
||||
@ -171,27 +164,29 @@ namespace osu.Game.Skinning
|
||||
}
|
||||
else
|
||||
{
|
||||
InternalChild = skin.GetAnimation("scorebar-colour", true, true, startAtCurrentTime: false, applyConfigFrameRate: true) ?? Drawable.Empty();
|
||||
InternalChild = skin.GetAnimation("scorebar-colour", true, true, startAtCurrentTime: false, applyConfigFrameRate: true) ?? Empty();
|
||||
Size = new Vector2(firstFrame.DisplayWidth, firstFrame.DisplayHeight);
|
||||
}
|
||||
|
||||
Position = new Vector2(3, 10) * 1.6f;
|
||||
Masking = true;
|
||||
}
|
||||
}
|
||||
|
||||
internal class LegacyNewStyleFill : LegacyHealthPiece
|
||||
internal class LegacyOldStyleFill : LegacyFill
|
||||
{
|
||||
public LegacyNewStyleFill(Skin skin)
|
||||
public LegacyOldStyleFill(ISkin skin)
|
||||
: base(skin)
|
||||
{
|
||||
InternalChild = new Sprite
|
||||
{
|
||||
Texture = getTexture(skin, "colour"),
|
||||
};
|
||||
Position = new Vector2(3, 10) * 1.6f;
|
||||
}
|
||||
}
|
||||
|
||||
Size = InternalChild.Size;
|
||||
internal class LegacyNewStyleFill : LegacyFill
|
||||
{
|
||||
public LegacyNewStyleFill(ISkin skin)
|
||||
: base(skin)
|
||||
{
|
||||
Position = new Vector2(7.5f, 7.8f) * 1.6f;
|
||||
Masking = true;
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
@ -254,7 +249,7 @@ namespace osu.Game.Skinning
|
||||
Main.ScaleTo(1.4f).Then().ScaleTo(1, 200, Easing.Out);
|
||||
}
|
||||
|
||||
public class LegacyHealthPiece : CompositeDrawable, IHealthDisplay
|
||||
public class LegacyHealthPiece : CompositeDrawable
|
||||
{
|
||||
public Bindable<double> Current { get; } = new Bindable<double>();
|
||||
|
||||
|
@ -72,7 +72,7 @@ namespace osu.Game.Skinning
|
||||
if (particles != null)
|
||||
{
|
||||
// start the particles already some way into their animation to break cluster away from centre.
|
||||
using (particles.BeginDelayedSequence(-100, true))
|
||||
using (particles.BeginDelayedSequence(-100))
|
||||
particles.Restart();
|
||||
}
|
||||
|
||||
|
@ -12,7 +12,6 @@ namespace osu.Game.Skinning
|
||||
/// </summary>
|
||||
public class LegacyRollingCounter : RollingCounter<int>
|
||||
{
|
||||
private readonly ISkin skin;
|
||||
private readonly LegacyFont font;
|
||||
|
||||
protected override bool IsRollingProportional => true;
|
||||
@ -20,11 +19,9 @@ namespace osu.Game.Skinning
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="LegacyRollingCounter"/>.
|
||||
/// </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>
|
||||
public LegacyRollingCounter(ISkin skin, LegacyFont font)
|
||||
public LegacyRollingCounter(LegacyFont font)
|
||||
{
|
||||
this.skin = skin;
|
||||
this.font = font;
|
||||
}
|
||||
|
||||
@ -33,6 +30,6 @@ namespace osu.Game.Skinning
|
||||
return Math.Abs(newValue - currentValue) * 75.0;
|
||||
}
|
||||
|
||||
protected sealed override OsuSpriteText CreateSpriteText() => new LegacySpriteText(skin, font);
|
||||
protected sealed override OsuSpriteText CreateSpriteText() => new LegacySpriteText(font);
|
||||
}
|
||||
}
|
||||
|
@ -1,39 +1,31 @@
|
||||
// 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.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Skinning
|
||||
{
|
||||
public class LegacyScoreCounter : ScoreCounter
|
||||
public class LegacyScoreCounter : GameplayScoreCounter, ISkinnableDrawable
|
||||
{
|
||||
private readonly ISkin skin;
|
||||
|
||||
protected override double RollingDuration => 1000;
|
||||
protected override Easing RollingEasing => Easing.Out;
|
||||
|
||||
public new Bindable<double> Current { get; } = new Bindable<double>();
|
||||
public bool UsesFixedAnchor { get; set; }
|
||||
|
||||
public LegacyScoreCounter(ISkin skin)
|
||||
public LegacyScoreCounter()
|
||||
: base(6)
|
||||
{
|
||||
Anchor = Anchor.TopRight;
|
||||
Origin = Anchor.TopRight;
|
||||
|
||||
this.skin = skin;
|
||||
|
||||
// base class uses int for display, but externally we bind to ScoreProcessor as a double for now.
|
||||
Current.BindValueChanged(v => base.Current.Value = (int)v.NewValue);
|
||||
|
||||
Scale = new Vector2(0.96f);
|
||||
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,
|
||||
Origin = Anchor.TopRight,
|
||||
|
@ -18,7 +18,9 @@ using osu.Game.Beatmaps.Formats;
|
||||
using osu.Game.IO;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osu.Game.Screens.Play.HUD.HitErrorMeters;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Skinning
|
||||
@ -53,15 +55,23 @@ namespace osu.Game.Skinning
|
||||
|
||||
private readonly Dictionary<int, LegacyManiaSkinConfiguration> maniaConfigurations = new Dictionary<int, LegacyManiaSkinConfiguration>();
|
||||
|
||||
[UsedImplicitly(ImplicitUseKindFlags.InstantiatedWithFixedConstructorSignature)]
|
||||
public LegacySkin(SkinInfo skin, IStorageResourceProvider resources)
|
||||
: this(skin, new LegacySkinResourceStore<SkinFileInfo>(skin, resources.Files), resources, "skin.ini")
|
||||
{
|
||||
}
|
||||
|
||||
protected LegacySkin(SkinInfo skin, [CanBeNull] IResourceStore<byte[]> storage, [CanBeNull] IStorageResourceProvider resources, string filename)
|
||||
: base(skin)
|
||||
/// <summary>
|
||||
/// Construct a new legacy skin instance.
|
||||
/// </summary>
|
||||
/// <param name="skin">The model for this skin.</param>
|
||||
/// <param name="storage">A storage for looking up files within this skin using user-facing filenames.</param>
|
||||
/// <param name="resources">Access to raw game resources.</param>
|
||||
/// <param name="configurationFilename">The user-facing filename of the configuration file to be parsed. Can accept an .osu or skin.ini file.</param>
|
||||
protected LegacySkin(SkinInfo skin, [CanBeNull] IResourceStore<byte[]> storage, [CanBeNull] IStorageResourceProvider resources, string configurationFilename)
|
||||
: base(skin, resources)
|
||||
{
|
||||
using (var stream = storage?.GetStream(filename))
|
||||
using (var stream = storage?.GetStream(configurationFilename))
|
||||
{
|
||||
if (stream != null)
|
||||
{
|
||||
@ -128,7 +138,7 @@ namespace osu.Game.Skinning
|
||||
|
||||
case LegacyManiaSkinConfigurationLookup maniaLookup:
|
||||
if (!AllowManiaSkin)
|
||||
return null;
|
||||
break;
|
||||
|
||||
var result = lookupForMania<TValue>(maniaLookup);
|
||||
if (result != null)
|
||||
@ -337,32 +347,73 @@ namespace osu.Game.Skinning
|
||||
|
||||
public override Drawable GetDrawableComponent(ISkinComponent component)
|
||||
{
|
||||
if (base.GetDrawableComponent(component) is Drawable c)
|
||||
return c;
|
||||
|
||||
switch (component)
|
||||
{
|
||||
case HUDSkinComponent hudComponent:
|
||||
{
|
||||
if (!this.HasFont(LegacyFont.Score))
|
||||
return null;
|
||||
|
||||
switch (hudComponent.Component)
|
||||
case SkinnableTargetComponent target:
|
||||
switch (target.Target)
|
||||
{
|
||||
case HUDSkinComponents.ComboCounter:
|
||||
return new LegacyComboCounter();
|
||||
case SkinnableTarget.MainHUDComponents:
|
||||
var skinnableTargetWrapper = new SkinnableTargetComponentsContainer(container =>
|
||||
{
|
||||
var score = container.OfType<LegacyScoreCounter>().FirstOrDefault();
|
||||
var accuracy = container.OfType<GameplayAccuracyCounter>().FirstOrDefault();
|
||||
var combo = container.OfType<LegacyComboCounter>().FirstOrDefault();
|
||||
|
||||
case HUDSkinComponents.ScoreCounter:
|
||||
return new LegacyScoreCounter(this);
|
||||
if (score != null && accuracy != null)
|
||||
{
|
||||
accuracy.Y = container.ToLocalSpace(score.ScreenSpaceDrawQuad.BottomRight).Y;
|
||||
}
|
||||
|
||||
case HUDSkinComponents.AccuracyCounter:
|
||||
return new LegacyAccuracyCounter(this);
|
||||
var songProgress = container.OfType<SongProgress>().FirstOrDefault();
|
||||
|
||||
case HUDSkinComponents.HealthDisplay:
|
||||
return new LegacyHealthDisplay(this);
|
||||
var hitError = container.OfType<HitErrorMeter>().FirstOrDefault();
|
||||
|
||||
if (hitError != null)
|
||||
{
|
||||
hitError.Anchor = Anchor.BottomCentre;
|
||||
hitError.Origin = Anchor.CentreLeft;
|
||||
hitError.Rotation = -90;
|
||||
}
|
||||
|
||||
if (songProgress != null)
|
||||
{
|
||||
if (hitError != null) hitError.Y -= SongProgress.MAX_HEIGHT;
|
||||
if (combo != null) combo.Y -= SongProgress.MAX_HEIGHT;
|
||||
}
|
||||
})
|
||||
{
|
||||
Children = this.HasFont(LegacyFont.Score)
|
||||
? new Drawable[]
|
||||
{
|
||||
new LegacyComboCounter(),
|
||||
new LegacyScoreCounter(),
|
||||
new LegacyAccuracyCounter(),
|
||||
new LegacyHealthDisplay(),
|
||||
new SongProgress(),
|
||||
new BarHitErrorMeter(),
|
||||
}
|
||||
: new Drawable[]
|
||||
{
|
||||
// TODO: these should fallback to using osu!classic skin textures, rather than doing this.
|
||||
new DefaultComboCounter(),
|
||||
new DefaultScoreCounter(),
|
||||
new DefaultAccuracyCounter(),
|
||||
new DefaultHealthDisplay(),
|
||||
new SongProgress(),
|
||||
new BarHitErrorMeter(),
|
||||
}
|
||||
};
|
||||
|
||||
return skinnableTargetWrapper;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
case GameplaySkinComponent<HitResult> resultComponent:
|
||||
// TODO: this should be inside the judgement pieces.
|
||||
Func<Drawable> createDrawable = () => getJudgementAnimation(resultComponent.Component);
|
||||
|
||||
// kind of wasteful that we throw this away, but should do for now.
|
||||
@ -458,7 +509,9 @@ namespace osu.Game.Skinning
|
||||
var sample = Samples?.Get(lookup);
|
||||
|
||||
if (sample != null)
|
||||
{
|
||||
return sample;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
@ -491,8 +544,7 @@ namespace osu.Game.Skinning
|
||||
yield return componentName;
|
||||
|
||||
// Fall back to using the last piece for components coming from lazer (e.g. "Gameplay/osu/approachcircle" -> "approachcircle").
|
||||
string lastPiece = componentName.Split('/').Last();
|
||||
yield return componentName.StartsWith("Gameplay/taiko/", StringComparison.Ordinal) ? "taiko-" + lastPiece : lastPiece;
|
||||
yield return componentName.Split('/').Last();
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
|
@ -27,6 +27,18 @@ namespace osu.Game.Skinning
|
||||
{
|
||||
Texture texture;
|
||||
|
||||
// find the first source which provides either the animated or non-animated version.
|
||||
ISkin skin = (source as ISkinSource)?.FindProvider(s =>
|
||||
{
|
||||
if (animatable && s.GetTexture(getFrameName(0)) != null)
|
||||
return true;
|
||||
|
||||
return s.GetTexture(componentName, wrapModeS, wrapModeT) != null;
|
||||
}) ?? source;
|
||||
|
||||
if (skin == null)
|
||||
return null;
|
||||
|
||||
if (animatable)
|
||||
{
|
||||
var textures = getTextures().ToArray();
|
||||
@ -35,7 +47,7 @@ namespace osu.Game.Skinning
|
||||
{
|
||||
var animation = new SkinnableTextureAnimation(startAtCurrentTime)
|
||||
{
|
||||
DefaultFrameLength = frameLength ?? getFrameLength(source, applyConfigFrameRate, textures),
|
||||
DefaultFrameLength = frameLength ?? getFrameLength(skin, applyConfigFrameRate, textures),
|
||||
Loop = looping,
|
||||
};
|
||||
|
||||
@ -47,7 +59,7 @@ namespace osu.Game.Skinning
|
||||
}
|
||||
|
||||
// if an animation was not allowed or not found, fall back to a sprite retrieval.
|
||||
if ((texture = source.GetTexture(componentName, wrapModeS, wrapModeT)) != null)
|
||||
if ((texture = skin.GetTexture(componentName, wrapModeS, wrapModeT)) != null)
|
||||
return new Sprite { Texture = texture };
|
||||
|
||||
return null;
|
||||
@ -56,12 +68,14 @@ namespace osu.Game.Skinning
|
||||
{
|
||||
for (int i = 0; true; i++)
|
||||
{
|
||||
if ((texture = source.GetTexture($"{componentName}{animationSeparator}{i}", wrapModeS, wrapModeT)) == null)
|
||||
if ((texture = skin.GetTexture(getFrameName(i), wrapModeS, wrapModeT)) == null)
|
||||
break;
|
||||
|
||||
yield return texture;
|
||||
}
|
||||
}
|
||||
|
||||
string getFrameName(int frameIndex) => $"{componentName}{animationSeparator}{frameIndex}";
|
||||
}
|
||||
|
||||
public static bool HasFont(this ISkin source, LegacyFont font)
|
||||
|
@ -1,6 +1,8 @@
|
||||
// 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 JetBrains.Annotations;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
@ -18,34 +20,35 @@ namespace osu.Game.Skinning
|
||||
public abstract class LegacySkinTransformer : ISkin
|
||||
{
|
||||
/// <summary>
|
||||
/// Source of the <see cref="ISkin"/> which is being transformed.
|
||||
/// The <see cref="ISkin"/> which is being transformed.
|
||||
/// </summary>
|
||||
protected ISkinSource Source { get; }
|
||||
[NotNull]
|
||||
protected ISkin Skin { get; }
|
||||
|
||||
protected LegacySkinTransformer(ISkinSource source)
|
||||
protected LegacySkinTransformer([NotNull] ISkin skin)
|
||||
{
|
||||
Source = source;
|
||||
Skin = skin ?? throw new ArgumentNullException(nameof(skin));
|
||||
}
|
||||
|
||||
public abstract Drawable GetDrawableComponent(ISkinComponent component);
|
||||
public virtual Drawable GetDrawableComponent(ISkinComponent component) => Skin.GetDrawableComponent(component);
|
||||
|
||||
public Texture GetTexture(string componentName) => GetTexture(componentName, default, default);
|
||||
|
||||
public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT)
|
||||
=> Source.GetTexture(componentName, wrapModeS, wrapModeT);
|
||||
=> Skin.GetTexture(componentName, wrapModeS, wrapModeT);
|
||||
|
||||
public virtual ISample GetSample(ISampleInfo sampleInfo)
|
||||
{
|
||||
if (!(sampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacySample))
|
||||
return Source.GetSample(sampleInfo);
|
||||
return Skin.GetSample(sampleInfo);
|
||||
|
||||
var playLayeredHitSounds = GetConfig<LegacySetting, bool>(LegacySetting.LayeredHitSounds);
|
||||
if (legacySample.IsLayered && playLayeredHitSounds?.Value == false)
|
||||
return new SampleVirtual();
|
||||
|
||||
return Source.GetSample(sampleInfo);
|
||||
return Skin.GetSample(sampleInfo);
|
||||
}
|
||||
|
||||
public abstract IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup);
|
||||
public virtual IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => Skin.GetConfig<TLookup, TValue>(lookup);
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Text;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
@ -9,19 +10,26 @@ using osuTK;
|
||||
|
||||
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[] FixedWidthExcludeCharacters => new[] { ',', '.', '%', 'x' };
|
||||
|
||||
public LegacySpriteText(ISkin skin, LegacyFont font)
|
||||
public LegacySpriteText(LegacyFont font)
|
||||
{
|
||||
this.font = font;
|
||||
Shadow = false;
|
||||
UseFullGlyphHeight = false;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(ISkinSource skin)
|
||||
{
|
||||
Font = new FontUsage(skin.GetFontPrefix(font), 1, fixedWidth: true);
|
||||
Spacing = new Vector2(-skin.GetFontOverlap(font), 0);
|
||||
|
||||
|
@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
@ -70,33 +71,50 @@ namespace osu.Game.Skinning
|
||||
updateSample();
|
||||
}
|
||||
|
||||
protected override void SkinChanged(ISkinSource skin, bool allowFallback)
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.SkinChanged(skin, allowFallback);
|
||||
base.LoadComplete();
|
||||
|
||||
CurrentSkin.SourceChanged += skinChangedImmediate;
|
||||
}
|
||||
|
||||
private void skinChangedImmediate()
|
||||
{
|
||||
// Clean up the previous sample immediately on a source change.
|
||||
// This avoids a potential call to Play() of an already disposed sample (samples are disposed along with the skin, but SkinChanged is scheduled).
|
||||
clearPreviousSamples();
|
||||
}
|
||||
|
||||
protected override void SkinChanged(ISkinSource skin)
|
||||
{
|
||||
base.SkinChanged(skin);
|
||||
updateSample();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether this sample was playing before a skin source change.
|
||||
/// </summary>
|
||||
private bool wasPlaying;
|
||||
|
||||
private void clearPreviousSamples()
|
||||
{
|
||||
// only run if the samples aren't already cleared.
|
||||
// this ensures the "wasPlaying" state is stored correctly even if multiple clear calls are executed.
|
||||
if (!sampleContainer.Any()) return;
|
||||
|
||||
wasPlaying = Playing;
|
||||
|
||||
sampleContainer.Clear();
|
||||
Sample = null;
|
||||
}
|
||||
|
||||
private void updateSample()
|
||||
{
|
||||
if (sampleInfo == null)
|
||||
return;
|
||||
|
||||
bool wasPlaying = Playing;
|
||||
|
||||
sampleContainer.Clear();
|
||||
Sample = null;
|
||||
|
||||
var sample = CurrentSkin.GetSample(sampleInfo);
|
||||
|
||||
if (sample == null && AllowDefaultFallback)
|
||||
{
|
||||
foreach (var lookup in sampleInfo.LookupNames)
|
||||
{
|
||||
if ((sample = sampleStore.Get(lookup)) != null)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (sample == null)
|
||||
return;
|
||||
|
||||
@ -155,6 +173,14 @@ namespace osu.Game.Skinning
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (CurrentSkin != null)
|
||||
CurrentSkin.SourceChanged -= skinChangedImmediate;
|
||||
}
|
||||
|
||||
#region Re-expose AudioContainer
|
||||
|
||||
public BindableNumber<double> Volume => sampleContainer.Volume;
|
||||
|
57
osu.Game/Skinning/ResourceStoreBackedSkin.cs
Normal file
57
osu.Game/Skinning/ResourceStoreBackedSkin.cs
Normal file
@ -0,0 +1,57 @@
|
||||
// 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 osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.OpenGL.Textures;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.IO.Stores;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Audio;
|
||||
|
||||
namespace osu.Game.Skinning
|
||||
{
|
||||
/// <summary>
|
||||
/// An <see cref="ISkin"/> that uses an underlying <see cref="IResourceStore{T}"/> with namespaces for resources retrieval.
|
||||
/// </summary>
|
||||
public class ResourceStoreBackedSkin : ISkin, IDisposable
|
||||
{
|
||||
private readonly TextureStore textures;
|
||||
private readonly ISampleStore samples;
|
||||
|
||||
public ResourceStoreBackedSkin(IResourceStore<byte[]> resources, GameHost host, AudioManager audio)
|
||||
{
|
||||
textures = new TextureStore(host.CreateTextureLoaderStore(new NamespacedResourceStore<byte[]>(resources, @"Textures")));
|
||||
samples = audio.GetSampleStore(new NamespacedResourceStore<byte[]>(resources, @"Samples"));
|
||||
}
|
||||
|
||||
public Drawable? GetDrawableComponent(ISkinComponent component) => null;
|
||||
|
||||
public Texture? GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => textures.Get(componentName, wrapModeS, wrapModeT);
|
||||
|
||||
public ISample? GetSample(ISampleInfo sampleInfo)
|
||||
{
|
||||
foreach (var lookup in sampleInfo.LookupNames)
|
||||
{
|
||||
ISample? sample = samples.Get(lookup);
|
||||
if (sample != null)
|
||||
return sample;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public IBindable<TValue>? GetConfig<TLookup, TValue>(TLookup lookup) => null;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
textures.Dispose();
|
||||
samples.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
117
osu.Game/Skinning/RulesetSkinProvidingContainer.cs
Normal file
117
osu.Game/Skinning/RulesetSkinProvidingContainer.cs
Normal file
@ -0,0 +1,117 @@
|
||||
// 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.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.IO.Stores;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.UI;
|
||||
|
||||
namespace osu.Game.Skinning
|
||||
{
|
||||
/// <summary>
|
||||
/// A type of <see cref="SkinProvidingContainer"/> specialized for <see cref="DrawableRuleset"/> and other gameplay-related components.
|
||||
/// Providing access to parent skin sources and the beatmap skin each surrounded with the ruleset legacy skin transformer.
|
||||
/// </summary>
|
||||
public class RulesetSkinProvidingContainer : SkinProvidingContainer
|
||||
{
|
||||
protected readonly Ruleset Ruleset;
|
||||
protected readonly IBeatmap Beatmap;
|
||||
|
||||
/// <remarks>
|
||||
/// This container already re-exposes all parent <see cref="ISkinSource"/> sources in a ruleset-usable form.
|
||||
/// Therefore disallow falling back to any parent <see cref="ISkinSource"/> any further.
|
||||
/// </remarks>
|
||||
protected override bool AllowFallingBackToParent => false;
|
||||
|
||||
protected override Container<Drawable> Content { get; }
|
||||
|
||||
public RulesetSkinProvidingContainer(Ruleset ruleset, IBeatmap beatmap, [CanBeNull] ISkin beatmapSkin)
|
||||
{
|
||||
Ruleset = ruleset;
|
||||
Beatmap = beatmap;
|
||||
|
||||
InternalChild = new BeatmapSkinProvidingContainer(beatmapSkin is LegacySkin ? GetLegacyRulesetTransformedSkin(beatmapSkin) : beatmapSkin)
|
||||
{
|
||||
Child = Content = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private ResourceStoreBackedSkin rulesetResourcesSkin;
|
||||
|
||||
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
|
||||
{
|
||||
if (Ruleset.CreateResourceStore() is IResourceStore<byte[]> resources)
|
||||
rulesetResourcesSkin = new ResourceStoreBackedSkin(resources, parent.Get<GameHost>(), parent.Get<AudioManager>());
|
||||
|
||||
return base.CreateChildDependencies(parent);
|
||||
}
|
||||
|
||||
protected override void OnSourceChanged()
|
||||
{
|
||||
ResetSources();
|
||||
|
||||
// Populate a local list first so we can adjust the returned order as we go.
|
||||
var sources = new List<ISkin>();
|
||||
|
||||
Debug.Assert(ParentSource != null);
|
||||
|
||||
foreach (var skin in ParentSource.AllSources)
|
||||
{
|
||||
switch (skin)
|
||||
{
|
||||
case LegacySkin legacySkin:
|
||||
sources.Add(GetLegacyRulesetTransformedSkin(legacySkin));
|
||||
break;
|
||||
|
||||
default:
|
||||
sources.Add(skin);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int lastDefaultSkinIndex = sources.IndexOf(sources.OfType<DefaultSkin>().LastOrDefault());
|
||||
|
||||
// Ruleset resources should be given the ability to override game-wide defaults
|
||||
// This is achieved by placing them before the last instance of DefaultSkin.
|
||||
// Note that DefaultSkin may not be present in some test scenes.
|
||||
if (lastDefaultSkinIndex >= 0)
|
||||
sources.Insert(lastDefaultSkinIndex, rulesetResourcesSkin);
|
||||
else
|
||||
sources.Add(rulesetResourcesSkin);
|
||||
|
||||
foreach (var skin in sources)
|
||||
AddSource(skin);
|
||||
}
|
||||
|
||||
protected ISkin GetLegacyRulesetTransformedSkin(ISkin legacySkin)
|
||||
{
|
||||
if (legacySkin == null)
|
||||
return null;
|
||||
|
||||
var rulesetTransformed = Ruleset.CreateLegacySkinProvider(legacySkin, Beatmap);
|
||||
if (rulesetTransformed != null)
|
||||
return rulesetTransformed;
|
||||
|
||||
return legacySkin;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
rulesetResourcesSkin?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
@ -2,12 +2,18 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Newtonsoft.Json;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.OpenGL.Textures;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.IO;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
|
||||
namespace osu.Game.Skinning
|
||||
{
|
||||
@ -17,7 +23,9 @@ namespace osu.Game.Skinning
|
||||
|
||||
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);
|
||||
|
||||
@ -27,9 +35,69 @@ namespace osu.Game.Skinning
|
||||
|
||||
public abstract IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup);
|
||||
|
||||
protected Skin(SkinInfo skin)
|
||||
protected Skin(SkinInfo skin, IStorageResourceProvider resources)
|
||||
{
|
||||
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);
|
||||
var deserializedContent = JsonConvert.DeserializeObject<IEnumerable<SkinnableInfo>>(jsonContent);
|
||||
|
||||
if (deserializedContent == null)
|
||||
continue;
|
||||
|
||||
DrawableComponentInfo[skinnableTarget] = deserializedContent.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
|
||||
|
@ -3,8 +3,11 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.IO;
|
||||
|
||||
namespace osu.Game.Skinning
|
||||
{
|
||||
@ -22,7 +25,19 @@ namespace osu.Game.Skinning
|
||||
|
||||
public string Creator { get; set; }
|
||||
|
||||
public List<SkinFileInfo> Files { get; set; }
|
||||
public string InstantiationInfo { get; set; }
|
||||
|
||||
public virtual Skin CreateInstance(IStorageResourceProvider resources)
|
||||
{
|
||||
var type = string.IsNullOrEmpty(InstantiationInfo)
|
||||
// handle the case of skins imported before InstantiationInfo was added.
|
||||
? typeof(LegacySkin)
|
||||
: Type.GetType(InstantiationInfo).AsNonNull();
|
||||
|
||||
return (Skin)Activator.CreateInstance(type, this, resources);
|
||||
}
|
||||
|
||||
public List<SkinFileInfo> Files { get; set; } = new List<SkinFileInfo>();
|
||||
|
||||
public List<DatabasedSetting> Settings { get; set; }
|
||||
|
||||
@ -31,8 +46,9 @@ namespace osu.Game.Skinning
|
||||
public static SkinInfo Default { get; } = new SkinInfo
|
||||
{
|
||||
ID = DEFAULT_SKIN,
|
||||
Name = "osu!lazer",
|
||||
Creator = "team osu!"
|
||||
Name = "osu! (triangles)",
|
||||
Creator = "team osu!",
|
||||
InstantiationInfo = typeof(DefaultSkin).GetInvariantInstantiationInfo()
|
||||
};
|
||||
|
||||
public bool Equals(SkinInfo other) => other != null && ID == other.ID;
|
||||
|
@ -6,9 +6,11 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Newtonsoft.Json;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Bindables;
|
||||
@ -22,11 +24,19 @@ using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.IO;
|
||||
using osu.Game.IO.Archives;
|
||||
|
||||
namespace osu.Game.Skinning
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles the storage and retrieval of <see cref="Skin"/>s.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is also exposed and cached as <see cref="ISkinSource"/> to allow for any component to potentially have skinning support.
|
||||
/// For gameplay components, see <see cref="RulesetSkinProvidingContainer"/> which adds extra legacy and toggle logic that may affect the lookup process.
|
||||
/// </remarks>
|
||||
[ExcludeFromDynamicCompile]
|
||||
public class SkinManager : ArchiveModelManager<SkinInfo, SkinFileInfo>, ISkinSource, IStorageResourceProvider
|
||||
{
|
||||
@ -34,9 +44,9 @@ namespace osu.Game.Skinning
|
||||
|
||||
private readonly GameHost host;
|
||||
|
||||
private readonly IResourceStore<byte[]> legacyDefaultResources;
|
||||
private readonly IResourceStore<byte[]> resources;
|
||||
|
||||
public readonly Bindable<Skin> CurrentSkin = new Bindable<Skin>(new DefaultSkin());
|
||||
public readonly Bindable<Skin> CurrentSkin = new Bindable<Skin>();
|
||||
public readonly Bindable<SkinInfo> CurrentSkinInfo = new Bindable<SkinInfo>(SkinInfo.Default) { Default = SkinInfo.Default };
|
||||
|
||||
public override IEnumerable<string> HandledExtensions => new[] { ".osk" };
|
||||
@ -45,15 +55,29 @@ namespace osu.Game.Skinning
|
||||
|
||||
protected override string ImportFromStablePath => "Skins";
|
||||
|
||||
public SkinManager(Storage storage, DatabaseContextFactory contextFactory, GameHost host, AudioManager audio, IResourceStore<byte[]> legacyDefaultResources)
|
||||
/// <summary>
|
||||
/// The default skin.
|
||||
/// </summary>
|
||||
public Skin DefaultSkin { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The default legacy skin.
|
||||
/// </summary>
|
||||
public Skin DefaultLegacySkin { get; }
|
||||
|
||||
public SkinManager(Storage storage, DatabaseContextFactory contextFactory, GameHost host, IResourceStore<byte[]> resources, AudioManager audio)
|
||||
: base(storage, contextFactory, new SkinStore(contextFactory, storage), host)
|
||||
{
|
||||
this.audio = audio;
|
||||
this.host = host;
|
||||
this.resources = resources;
|
||||
|
||||
this.legacyDefaultResources = legacyDefaultResources;
|
||||
DefaultLegacySkin = new DefaultLegacySkin(this);
|
||||
DefaultSkin = new DefaultSkin(this);
|
||||
|
||||
CurrentSkinInfo.ValueChanged += skin => CurrentSkin.Value = GetSkin(skin.NewValue);
|
||||
|
||||
CurrentSkin.Value = DefaultSkin;
|
||||
CurrentSkin.ValueChanged += skin =>
|
||||
{
|
||||
if (skin.NewValue.SkinInfo != CurrentSkinInfo.Value)
|
||||
@ -72,8 +96,8 @@ namespace osu.Game.Skinning
|
||||
public List<SkinInfo> GetAllUsableSkins()
|
||||
{
|
||||
var userSkins = GetAllUserSkins();
|
||||
userSkins.Insert(0, SkinInfo.Default);
|
||||
userSkins.Insert(1, DefaultLegacySkin.Info);
|
||||
userSkins.Insert(0, DefaultSkin.SkinInfo);
|
||||
userSkins.Insert(1, DefaultLegacySkin.SkinInfo);
|
||||
return userSkins;
|
||||
}
|
||||
|
||||
@ -101,11 +125,13 @@ namespace osu.Game.Skinning
|
||||
|
||||
private const string unknown_creator_string = "Unknown";
|
||||
|
||||
protected override bool HasCustomHashFunction => true;
|
||||
|
||||
protected override string ComputeHash(SkinInfo item, ArchiveReader reader = null)
|
||||
{
|
||||
// we need to populate early to create a hash based off skin.ini contents
|
||||
if (item.Name?.Contains(".osk", StringComparison.OrdinalIgnoreCase) == true)
|
||||
populateMetadata(item);
|
||||
populateMetadata(item, GetSkin(item));
|
||||
|
||||
if (item.Creator != null && item.Creator != unknown_creator_string)
|
||||
{
|
||||
@ -118,22 +144,24 @@ namespace osu.Game.Skinning
|
||||
return base.ComputeHash(item, reader);
|
||||
}
|
||||
|
||||
protected override async Task Populate(SkinInfo model, ArchiveReader archive, CancellationToken cancellationToken = default)
|
||||
protected override Task Populate(SkinInfo model, ArchiveReader archive, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await base.Populate(model, archive, cancellationToken).ConfigureAwait(false);
|
||||
var instance = GetSkin(model);
|
||||
|
||||
model.InstantiationInfo ??= instance.GetType().GetInvariantInstantiationInfo();
|
||||
|
||||
if (model.Name?.Contains(".osk", StringComparison.OrdinalIgnoreCase) == true)
|
||||
populateMetadata(model);
|
||||
populateMetadata(model, instance);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void populateMetadata(SkinInfo item)
|
||||
private void populateMetadata(SkinInfo item, Skin instance)
|
||||
{
|
||||
Skin reference = GetSkin(item);
|
||||
|
||||
if (!string.IsNullOrEmpty(reference.Configuration.SkinInfo.Name))
|
||||
if (!string.IsNullOrEmpty(instance.Configuration.SkinInfo.Name))
|
||||
{
|
||||
item.Name = reference.Configuration.SkinInfo.Name;
|
||||
item.Creator = reference.Configuration.SkinInfo.Creator;
|
||||
item.Name = instance.Configuration.SkinInfo.Name;
|
||||
item.Creator = instance.Configuration.SkinInfo.Creator;
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -147,15 +175,48 @@ namespace osu.Game.Skinning
|
||||
/// </summary>
|
||||
/// <param name="skinInfo">The skin to lookup.</param>
|
||||
/// <returns>A <see cref="Skin"/> instance correlating to the provided <see cref="SkinInfo"/>.</returns>
|
||||
public Skin GetSkin(SkinInfo skinInfo)
|
||||
public Skin GetSkin(SkinInfo skinInfo) => skinInfo.CreateInstance(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 (skinInfo == SkinInfo.Default)
|
||||
return new DefaultSkin();
|
||||
if (CurrentSkinInfo.Value.ID >= 1) return;
|
||||
|
||||
if (skinInfo == DefaultLegacySkin.Info)
|
||||
return new DefaultLegacySkin(legacyDefaultResources, this);
|
||||
var skin = CurrentSkin.Value;
|
||||
|
||||
return new LegacySkin(skinInfo, this);
|
||||
// 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>
|
||||
@ -167,17 +228,55 @@ namespace osu.Game.Skinning
|
||||
|
||||
public event Action SourceChanged;
|
||||
|
||||
public Drawable GetDrawableComponent(ISkinComponent component) => CurrentSkin.Value.GetDrawableComponent(component);
|
||||
public Drawable GetDrawableComponent(ISkinComponent component) => lookupWithFallback(s => s.GetDrawableComponent(component));
|
||||
|
||||
public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => CurrentSkin.Value.GetTexture(componentName, wrapModeS, wrapModeT);
|
||||
public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => lookupWithFallback(s => s.GetTexture(componentName, wrapModeS, wrapModeT));
|
||||
|
||||
public ISample GetSample(ISampleInfo sampleInfo) => CurrentSkin.Value.GetSample(sampleInfo);
|
||||
public ISample GetSample(ISampleInfo sampleInfo) => lookupWithFallback(s => s.GetSample(sampleInfo));
|
||||
|
||||
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => CurrentSkin.Value.GetConfig<TLookup, TValue>(lookup);
|
||||
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => lookupWithFallback(s => s.GetConfig<TLookup, TValue>(lookup));
|
||||
|
||||
public ISkin FindProvider(Func<ISkin, bool> lookupFunction)
|
||||
{
|
||||
foreach (var source in AllSources)
|
||||
{
|
||||
if (lookupFunction(source))
|
||||
return source;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public IEnumerable<ISkin> AllSources
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return CurrentSkin.Value;
|
||||
|
||||
if (CurrentSkin.Value is LegacySkin && CurrentSkin.Value != DefaultLegacySkin)
|
||||
yield return DefaultLegacySkin;
|
||||
|
||||
if (CurrentSkin.Value != DefaultSkin)
|
||||
yield return DefaultSkin;
|
||||
}
|
||||
}
|
||||
|
||||
private T lookupWithFallback<T>(Func<ISkin, T> lookupFunction)
|
||||
where T : class
|
||||
{
|
||||
foreach (var source in AllSources)
|
||||
{
|
||||
if (lookupFunction(source) is T skinSourced)
|
||||
return skinSourced;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
#region IResourceStorageProvider
|
||||
|
||||
AudioManager IStorageResourceProvider.AudioManager => audio;
|
||||
IResourceStore<byte[]> IStorageResourceProvider.Resources => resources;
|
||||
IResourceStore<byte[]> IStorageResourceProvider.Files => Files.Store;
|
||||
IResourceStore<TextureUpload> IStorageResourceProvider.CreateTextureLoaderStore(IResourceStore<byte[]> underlyingStore) => host.CreateTextureLoaderStore(underlyingStore);
|
||||
|
||||
|
@ -2,6 +2,8 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Bindables;
|
||||
@ -20,9 +22,13 @@ namespace osu.Game.Skinning
|
||||
{
|
||||
public event Action SourceChanged;
|
||||
|
||||
private readonly ISkin skin;
|
||||
[CanBeNull]
|
||||
protected ISkinSource ParentSource { get; private set; }
|
||||
|
||||
private ISkinSource fallbackSource;
|
||||
/// <summary>
|
||||
/// Whether falling back to parent <see cref="ISkinSource"/>s is allowed in this container.
|
||||
/// </summary>
|
||||
protected virtual bool AllowFallingBackToParent => true;
|
||||
|
||||
protected virtual bool AllowDrawableLookup(ISkinComponent component) => true;
|
||||
|
||||
@ -34,80 +40,181 @@ namespace osu.Game.Skinning
|
||||
|
||||
protected virtual bool AllowColourLookup => true;
|
||||
|
||||
public SkinProvidingContainer(ISkin skin)
|
||||
{
|
||||
this.skin = skin;
|
||||
/// <summary>
|
||||
/// A dictionary mapping each <see cref="ISkin"/> source to a wrapper which handles lookup allowances.
|
||||
/// </summary>
|
||||
private readonly List<(ISkin skin, DisableableSkinSource wrapped)> skinSources = new List<(ISkin, DisableableSkinSource)>();
|
||||
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
}
|
||||
|
||||
public Drawable GetDrawableComponent(ISkinComponent component)
|
||||
{
|
||||
Drawable sourceDrawable;
|
||||
if (AllowDrawableLookup(component) && (sourceDrawable = skin?.GetDrawableComponent(component)) != null)
|
||||
return sourceDrawable;
|
||||
|
||||
return fallbackSource?.GetDrawableComponent(component);
|
||||
}
|
||||
|
||||
public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT)
|
||||
{
|
||||
Texture sourceTexture;
|
||||
if (AllowTextureLookup(componentName) && (sourceTexture = skin?.GetTexture(componentName, wrapModeS, wrapModeT)) != null)
|
||||
return sourceTexture;
|
||||
|
||||
return fallbackSource?.GetTexture(componentName, wrapModeS, wrapModeT);
|
||||
}
|
||||
|
||||
public ISample GetSample(ISampleInfo sampleInfo)
|
||||
{
|
||||
ISample sourceChannel;
|
||||
if (AllowSampleLookup(sampleInfo) && (sourceChannel = skin?.GetSample(sampleInfo)) != null)
|
||||
return sourceChannel;
|
||||
|
||||
return fallbackSource?.GetSample(sampleInfo);
|
||||
}
|
||||
|
||||
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup)
|
||||
/// <summary>
|
||||
/// Constructs a new <see cref="SkinProvidingContainer"/> initialised with a single skin source.
|
||||
/// </summary>
|
||||
public SkinProvidingContainer([CanBeNull] ISkin skin)
|
||||
: this()
|
||||
{
|
||||
if (skin != null)
|
||||
{
|
||||
if (lookup is GlobalSkinColours || lookup is SkinComboColourLookup || lookup is SkinCustomColourLookup)
|
||||
return lookupWithFallback<TLookup, TValue>(lookup, AllowColourLookup);
|
||||
|
||||
return lookupWithFallback<TLookup, TValue>(lookup, AllowConfigurationLookup);
|
||||
}
|
||||
|
||||
return fallbackSource?.GetConfig<TLookup, TValue>(lookup);
|
||||
AddSource(skin);
|
||||
}
|
||||
|
||||
private IBindable<TValue> lookupWithFallback<TLookup, TValue>(TLookup lookup, bool canUseSkinLookup)
|
||||
/// <summary>
|
||||
/// Constructs a new <see cref="SkinProvidingContainer"/> with no sources.
|
||||
/// </summary>
|
||||
protected SkinProvidingContainer()
|
||||
{
|
||||
if (canUseSkinLookup)
|
||||
{
|
||||
var bindable = skin.GetConfig<TLookup, TValue>(lookup);
|
||||
if (bindable != null)
|
||||
return bindable;
|
||||
}
|
||||
|
||||
return fallbackSource?.GetConfig<TLookup, TValue>(lookup);
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
}
|
||||
|
||||
protected virtual void TriggerSourceChanged() => SourceChanged?.Invoke();
|
||||
|
||||
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
|
||||
{
|
||||
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
|
||||
|
||||
fallbackSource = dependencies.Get<ISkinSource>();
|
||||
if (fallbackSource != null)
|
||||
fallbackSource.SourceChanged += TriggerSourceChanged;
|
||||
ParentSource = dependencies.Get<ISkinSource>();
|
||||
if (ParentSource != null)
|
||||
ParentSource.SourceChanged += TriggerSourceChanged;
|
||||
|
||||
dependencies.CacheAs<ISkinSource>(this);
|
||||
|
||||
TriggerSourceChanged();
|
||||
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
public ISkin FindProvider(Func<ISkin, bool> lookupFunction)
|
||||
{
|
||||
foreach (var (skin, lookupWrapper) in skinSources)
|
||||
{
|
||||
if (lookupFunction(lookupWrapper))
|
||||
return skin;
|
||||
}
|
||||
|
||||
if (!AllowFallingBackToParent)
|
||||
return null;
|
||||
|
||||
return ParentSource?.FindProvider(lookupFunction);
|
||||
}
|
||||
|
||||
public IEnumerable<ISkin> AllSources
|
||||
{
|
||||
get
|
||||
{
|
||||
foreach (var i in skinSources)
|
||||
yield return i.skin;
|
||||
|
||||
if (AllowFallingBackToParent && ParentSource != null)
|
||||
{
|
||||
foreach (var skin in ParentSource.AllSources)
|
||||
yield return skin;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Drawable GetDrawableComponent(ISkinComponent component)
|
||||
{
|
||||
foreach (var (_, lookupWrapper) in skinSources)
|
||||
{
|
||||
Drawable sourceDrawable;
|
||||
if ((sourceDrawable = lookupWrapper.GetDrawableComponent(component)) != null)
|
||||
return sourceDrawable;
|
||||
}
|
||||
|
||||
if (!AllowFallingBackToParent)
|
||||
return null;
|
||||
|
||||
return ParentSource?.GetDrawableComponent(component);
|
||||
}
|
||||
|
||||
public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT)
|
||||
{
|
||||
foreach (var (_, lookupWrapper) in skinSources)
|
||||
{
|
||||
Texture sourceTexture;
|
||||
if ((sourceTexture = lookupWrapper.GetTexture(componentName, wrapModeS, wrapModeT)) != null)
|
||||
return sourceTexture;
|
||||
}
|
||||
|
||||
if (!AllowFallingBackToParent)
|
||||
return null;
|
||||
|
||||
return ParentSource?.GetTexture(componentName, wrapModeS, wrapModeT);
|
||||
}
|
||||
|
||||
public ISample GetSample(ISampleInfo sampleInfo)
|
||||
{
|
||||
foreach (var (_, lookupWrapper) in skinSources)
|
||||
{
|
||||
ISample sourceSample;
|
||||
if ((sourceSample = lookupWrapper.GetSample(sampleInfo)) != null)
|
||||
return sourceSample;
|
||||
}
|
||||
|
||||
if (!AllowFallingBackToParent)
|
||||
return null;
|
||||
|
||||
return ParentSource?.GetSample(sampleInfo);
|
||||
}
|
||||
|
||||
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup)
|
||||
{
|
||||
foreach (var (_, lookupWrapper) in skinSources)
|
||||
{
|
||||
IBindable<TValue> bindable;
|
||||
if ((bindable = lookupWrapper.GetConfig<TLookup, TValue>(lookup)) != null)
|
||||
return bindable;
|
||||
}
|
||||
|
||||
if (!AllowFallingBackToParent)
|
||||
return null;
|
||||
|
||||
return ParentSource?.GetConfig<TLookup, TValue>(lookup);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a new skin to this provider. Will be added to the end of the lookup order precedence.
|
||||
/// </summary>
|
||||
/// <param name="skin">The skin to add.</param>
|
||||
protected void AddSource(ISkin skin)
|
||||
{
|
||||
skinSources.Add((skin, new DisableableSkinSource(skin, this)));
|
||||
|
||||
if (skin is ISkinSource source)
|
||||
source.SourceChanged += TriggerSourceChanged;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove a skin from this provider.
|
||||
/// </summary>
|
||||
/// <param name="skin">The skin to remove.</param>
|
||||
protected void RemoveSource(ISkin skin)
|
||||
{
|
||||
if (skinSources.RemoveAll(s => s.skin == skin) == 0)
|
||||
return;
|
||||
|
||||
if (skin is ISkinSource source)
|
||||
source.SourceChanged -= TriggerSourceChanged;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all skin sources.
|
||||
/// </summary>
|
||||
protected void ResetSources()
|
||||
{
|
||||
foreach (var i in skinSources.ToArray())
|
||||
RemoveSource(i.skin);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when any source has changed (either <see cref="ParentSource"/> or a source registered via <see cref="AddSource"/>).
|
||||
/// This is also invoked once initially during <see cref="CreateChildDependencies"/> to ensure sources are ready for children consumption.
|
||||
/// </summary>
|
||||
protected virtual void OnSourceChanged() { }
|
||||
|
||||
protected void TriggerSourceChanged()
|
||||
{
|
||||
// Expose to implementations, giving them a chance to react before notifying external consumers.
|
||||
OnSourceChanged();
|
||||
|
||||
SourceChanged?.Invoke();
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
// Must be done before base.Dispose()
|
||||
@ -115,8 +222,72 @@ namespace osu.Game.Skinning
|
||||
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (fallbackSource != null)
|
||||
fallbackSource.SourceChanged -= TriggerSourceChanged;
|
||||
if (ParentSource != null)
|
||||
ParentSource.SourceChanged -= TriggerSourceChanged;
|
||||
|
||||
foreach (var i in skinSources)
|
||||
{
|
||||
if (i.skin is ISkinSource source)
|
||||
source.SourceChanged -= TriggerSourceChanged;
|
||||
}
|
||||
}
|
||||
|
||||
private class DisableableSkinSource : ISkin
|
||||
{
|
||||
private readonly ISkin skin;
|
||||
private readonly SkinProvidingContainer provider;
|
||||
|
||||
public DisableableSkinSource(ISkin skin, SkinProvidingContainer provider)
|
||||
{
|
||||
this.skin = skin;
|
||||
this.provider = provider;
|
||||
}
|
||||
|
||||
public Drawable GetDrawableComponent(ISkinComponent component)
|
||||
{
|
||||
if (provider.AllowDrawableLookup(component))
|
||||
return skin.GetDrawableComponent(component);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT)
|
||||
{
|
||||
if (provider.AllowTextureLookup(componentName))
|
||||
return skin.GetTexture(componentName, wrapModeS, wrapModeT);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public ISample GetSample(ISampleInfo sampleInfo)
|
||||
{
|
||||
if (provider.AllowSampleLookup(sampleInfo))
|
||||
return skin.GetSample(sampleInfo);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup)
|
||||
{
|
||||
switch (lookup)
|
||||
{
|
||||
case GlobalSkinColours _:
|
||||
case SkinComboColourLookup _:
|
||||
case SkinCustomColourLookup _:
|
||||
if (provider.AllowColourLookup)
|
||||
return skin.GetConfig<TLookup, TValue>(lookup);
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
if (provider.AllowConfigurationLookup)
|
||||
return skin.GetConfig<TLookup, TValue>(lookup);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -22,22 +22,6 @@ namespace osu.Game.Skinning
|
||||
/// </summary>
|
||||
protected ISkinSource CurrentSkin { get; private set; }
|
||||
|
||||
private readonly Func<ISkinSource, bool> allowFallback;
|
||||
|
||||
/// <summary>
|
||||
/// Whether fallback to default skin should be allowed if the custom skin is missing this resource.
|
||||
/// </summary>
|
||||
protected bool AllowDefaultFallback => allowFallback == null || allowFallback.Invoke(CurrentSkin);
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="SkinReloadableDrawable"/>
|
||||
/// </summary>
|
||||
/// <param name="allowFallback">A conditional to decide whether to allow fallback to the default implementation if a skinned element is not present.</param>
|
||||
protected SkinReloadableDrawable(Func<ISkinSource, bool> allowFallback = null)
|
||||
{
|
||||
this.allowFallback = allowFallback;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(ISkinSource source)
|
||||
{
|
||||
@ -58,7 +42,7 @@ namespace osu.Game.Skinning
|
||||
|
||||
private void skinChanged()
|
||||
{
|
||||
SkinChanged(CurrentSkin, AllowDefaultFallback);
|
||||
SkinChanged(CurrentSkin);
|
||||
OnSkinChanged?.Invoke();
|
||||
}
|
||||
|
||||
@ -66,8 +50,7 @@ namespace osu.Game.Skinning
|
||||
/// Called when a change is made to the skin.
|
||||
/// </summary>
|
||||
/// <param name="skin">The new skin.</param>
|
||||
/// <param name="allowFallback">Whether fallback to default skin should be allowed if the custom skin is missing this resource.</param>
|
||||
protected virtual void SkinChanged(ISkinSource skin, bool allowFallback)
|
||||
protected virtual void SkinChanged(ISkinSource skin)
|
||||
{
|
||||
}
|
||||
|
||||
|
@ -23,7 +23,7 @@ namespace osu.Game.Skinning
|
||||
/// Whether the drawable component should be centered in available space.
|
||||
/// Defaults to true.
|
||||
/// </summary>
|
||||
public bool CentreComponent { get; set; } = true;
|
||||
public bool CentreComponent = true;
|
||||
|
||||
public new Axes AutoSizeAxes
|
||||
{
|
||||
@ -40,16 +40,14 @@ namespace osu.Game.Skinning
|
||||
/// </summary>
|
||||
/// <param name="component">The namespace-complete resource name for this skinnable element.</param>
|
||||
/// <param name="defaultImplementation">A function to create the default skin implementation of this element.</param>
|
||||
/// <param name="allowFallback">A conditional to decide whether to allow fallback to the default implementation if a skinned element is not present.</param>
|
||||
/// <param name="confineMode">How (if at all) the <see cref="Drawable"/> should be resize to fit within our own bounds.</param>
|
||||
public SkinnableDrawable(ISkinComponent component, Func<ISkinComponent, Drawable> defaultImplementation, Func<ISkinSource, bool> allowFallback = null, ConfineMode confineMode = ConfineMode.NoScaling)
|
||||
: this(component, allowFallback, confineMode)
|
||||
public SkinnableDrawable(ISkinComponent component, Func<ISkinComponent, Drawable> defaultImplementation = null, ConfineMode confineMode = ConfineMode.NoScaling)
|
||||
: this(component, confineMode)
|
||||
{
|
||||
createDefault = defaultImplementation;
|
||||
}
|
||||
|
||||
protected SkinnableDrawable(ISkinComponent component, Func<ISkinSource, bool> allowFallback = null, ConfineMode confineMode = ConfineMode.NoScaling)
|
||||
: base(allowFallback)
|
||||
protected SkinnableDrawable(ISkinComponent component, ConfineMode confineMode = ConfineMode.NoScaling)
|
||||
{
|
||||
this.component = component;
|
||||
this.confineMode = confineMode;
|
||||
@ -68,20 +66,20 @@ namespace osu.Game.Skinning
|
||||
|
||||
private bool isDefault;
|
||||
|
||||
protected virtual Drawable CreateDefault(ISkinComponent component) => createDefault(component);
|
||||
protected virtual Drawable CreateDefault(ISkinComponent component) => createDefault?.Invoke(component) ?? Empty();
|
||||
|
||||
/// <summary>
|
||||
/// Whether to apply size restrictions (specified via <see cref="confineMode"/>) to the default implementation.
|
||||
/// </summary>
|
||||
protected virtual bool ApplySizeRestrictionsToDefault => false;
|
||||
|
||||
protected override void SkinChanged(ISkinSource skin, bool allowFallback)
|
||||
protected override void SkinChanged(ISkinSource skin)
|
||||
{
|
||||
Drawable = skin.GetDrawableComponent(component);
|
||||
|
||||
isDefault = false;
|
||||
|
||||
if (Drawable == null && allowFallback)
|
||||
if (Drawable == null)
|
||||
{
|
||||
Drawable = CreateDefault(component);
|
||||
isDefault = true;
|
||||
|
@ -1,7 +1,6 @@
|
||||
// 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.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
@ -19,8 +18,8 @@ namespace osu.Game.Skinning
|
||||
[Resolved]
|
||||
private TextureStore textures { get; set; }
|
||||
|
||||
public SkinnableSprite(string textureName, Func<ISkinSource, bool> allowFallback = null, ConfineMode confineMode = ConfineMode.NoScaling)
|
||||
: base(new SpriteComponent(textureName), allowFallback, confineMode)
|
||||
public SkinnableSprite(string textureName, ConfineMode confineMode = ConfineMode.NoScaling)
|
||||
: base(new SpriteComponent(textureName), confineMode)
|
||||
{
|
||||
}
|
||||
|
||||
|
@ -9,14 +9,14 @@ namespace osu.Game.Skinning
|
||||
{
|
||||
public class SkinnableSpriteText : SkinnableDrawable, IHasText
|
||||
{
|
||||
public SkinnableSpriteText(ISkinComponent component, Func<ISkinComponent, SpriteText> defaultImplementation, Func<ISkinSource, bool> allowFallback = null, ConfineMode confineMode = ConfineMode.NoScaling)
|
||||
: base(component, defaultImplementation, allowFallback, confineMode)
|
||||
public SkinnableSpriteText(ISkinComponent component, Func<ISkinComponent, SpriteText> defaultImplementation, ConfineMode confineMode = ConfineMode.NoScaling)
|
||||
: base(component, defaultImplementation, confineMode)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void SkinChanged(ISkinSource skin, bool allowFallback)
|
||||
protected override void SkinChanged(ISkinSource skin)
|
||||
{
|
||||
base.SkinChanged(skin, allowFallback);
|
||||
base.SkinChanged(skin);
|
||||
|
||||
if (Drawable is IHasText textDrawable)
|
||||
textDrawable.Text = Text;
|
||||
|
10
osu.Game/Skinning/SkinnableTarget.cs
Normal file
10
osu.Game/Skinning/SkinnableTarget.cs
Normal file
@ -0,0 +1,10 @@
|
||||
// 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 enum SkinnableTarget
|
||||
{
|
||||
MainHUDComponents
|
||||
}
|
||||
}
|
17
osu.Game/Skinning/SkinnableTargetComponent.cs
Normal file
17
osu.Game/Skinning/SkinnableTargetComponent.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
48
osu.Game/Skinning/SkinnableTargetComponentsContainer.cs
Normal file
48
osu.Game/Skinning/SkinnableTargetComponentsContainer.cs
Normal file
@ -0,0 +1,48 @@
|
||||
// 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;
|
||||
|
||||
public bool UsesFixedAnchor { get; set; }
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
89
osu.Game/Skinning/SkinnableTargetContainer.cs
Normal file
89
osu.Game/Skinning/SkinnableTargetContainer.cs
Normal file
@ -0,0 +1,89 @@
|
||||
// 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 bool ComponentsLoaded { get; private set; }
|
||||
|
||||
public SkinnableTargetContainer(SkinnableTarget target)
|
||||
{
|
||||
Target = target;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reload all components in this container from the current skin.
|
||||
/// </summary>
|
||||
public void Reload()
|
||||
{
|
||||
ClearInternal();
|
||||
components.Clear();
|
||||
ComponentsLoaded = false;
|
||||
|
||||
content = CurrentSkin.GetDrawableComponent(new SkinnableTargetComponent(Target)) as SkinnableTargetComponentsContainer;
|
||||
|
||||
if (content != null)
|
||||
{
|
||||
LoadComponentAsync(content, wrapper =>
|
||||
{
|
||||
AddInternal(wrapper);
|
||||
components.AddRange(wrapper.Children.OfType<ISkinnableDrawable>());
|
||||
ComponentsLoaded = true;
|
||||
});
|
||||
}
|
||||
else
|
||||
ComponentsLoaded = true;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="ISkinnableTarget"/>
|
||||
/// <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(component));
|
||||
|
||||
content.Add(drawable);
|
||||
components.Add(component);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="ISkinnableTarget"/>
|
||||
/// <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 Remove(ISkinnableDrawable component)
|
||||
{
|
||||
if (content == null)
|
||||
throw new NotSupportedException("Attempting to remove a new component from 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(component));
|
||||
|
||||
content.Remove(drawable);
|
||||
components.Remove(component);
|
||||
}
|
||||
|
||||
protected override void SkinChanged(ISkinSource skin)
|
||||
{
|
||||
base.SkinChanged(skin);
|
||||
|
||||
Reload();
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user