1
0
mirror of https://github.com/ppy/osu.git synced 2025-02-15 14:42:56 +08:00

Merge pull request #13796 from peppy/fix-skin-providing-container

Rewrite `SkinProvidingContainer`
This commit is contained in:
Dan Balasescu 2021-07-07 15:43:24 +09:00 committed by GitHub
commit 05c4e0254b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 212 additions and 121 deletions

View File

@ -0,0 +1,92 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Textures;
using osu.Game.Audio;
using osu.Game.Skinning;
using osu.Game.Tests.Visual;
namespace osu.Game.Tests.Skins
{
public class TestSceneSkinProvidingContainer : OsuTestScene
{
/// <summary>
/// Ensures that the first inserted skin after resetting (via source change)
/// is always prioritised over others when providing the same resource.
/// </summary>
[Test]
public void TestPriorityPreservation()
{
TestSkinProvidingContainer provider = null;
TestSkin mostPrioritisedSource = null;
AddStep("setup sources", () =>
{
var sources = new List<TestSkin>();
for (int i = 0; i < 10; i++)
sources.Add(new TestSkin());
mostPrioritisedSource = sources.First();
Child = provider = new TestSkinProvidingContainer(sources);
});
AddAssert("texture provided by expected skin", () =>
{
return provider.FindProvider(s => s.GetTexture(TestSkin.TEXTURE_NAME) != null) == mostPrioritisedSource;
});
AddStep("trigger source change", () => provider.TriggerSourceChanged());
AddAssert("texture still provided by expected skin", () =>
{
return provider.FindProvider(s => s.GetTexture(TestSkin.TEXTURE_NAME) != null) == mostPrioritisedSource;
});
}
private class TestSkinProvidingContainer : SkinProvidingContainer
{
private readonly IEnumerable<ISkin> sources;
public TestSkinProvidingContainer(IEnumerable<ISkin> sources)
{
this.sources = sources;
}
public new void TriggerSourceChanged() => base.TriggerSourceChanged();
protected override void OnSourceChanged()
{
ResetSources();
sources.ForEach(AddSource);
}
}
private class TestSkin : ISkin
{
public const string TEXTURE_NAME = "virtual-texture";
public Drawable GetDrawableComponent(ISkinComponent component) => throw new System.NotImplementedException();
public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT)
{
if (componentName == TEXTURE_NAME)
return Texture.WhitePixel;
return null;
}
public ISample GetSample(ISampleInfo sampleInfo) => throw new System.NotImplementedException();
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => throw new System.NotImplementedException();
}
}
}

View File

@ -168,7 +168,7 @@ namespace osu.Game.Tests.Visual.Gameplay
public void Disable()
{
allow = false;
OnSourceChanged();
TriggerSourceChanged();
}
public SwitchableSkinProvidingContainer(ISkin skin)

View File

@ -83,9 +83,9 @@ namespace osu.Game.Skinning
[BackgroundDependencyLoader]
private void load()
{
beatmapSkins.BindValueChanged(_ => OnSourceChanged());
beatmapColours.BindValueChanged(_ => OnSourceChanged());
beatmapHitsounds.BindValueChanged(_ => OnSourceChanged());
beatmapSkins.BindValueChanged(_ => TriggerSourceChanged());
beatmapColours.BindValueChanged(_ => TriggerSourceChanged());
beatmapHitsounds.BindValueChanged(_ => TriggerSourceChanged());
}
}
}

View File

@ -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.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
@ -46,57 +48,51 @@ namespace osu.Game.Skinning
};
}
private ISkinSource parentSource;
private ResourceStoreBackedSkin rulesetResourcesSkin;
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
parentSource = parent.Get<ISkinSource>();
parentSource.SourceChanged += OnSourceChanged;
if (Ruleset.CreateResourceStore() is IResourceStore<byte[]> resources)
rulesetResourcesSkin = new ResourceStoreBackedSkin(resources, parent.Get<GameHost>(), parent.Get<AudioManager>());
// ensure sources are populated and ready for use before childrens' asynchronous load flow.
UpdateSkinSources();
return base.CreateChildDependencies(parent);
}
protected override void OnSourceChanged()
{
UpdateSkinSources();
base.OnSourceChanged();
}
ResetSources();
protected virtual void UpdateSkinSources()
{
SkinSources.Clear();
// Populate a local list first so we can adjust the returned order as we go.
var sources = new List<ISkin>();
foreach (var skin in parentSource.AllSources)
Debug.Assert(ParentSource != null);
foreach (var skin in ParentSource.AllSources)
{
switch (skin)
{
case LegacySkin legacySkin:
SkinSources.Add(GetLegacyRulesetTransformedSkin(legacySkin));
sources.Add(GetLegacyRulesetTransformedSkin(legacySkin));
break;
default:
SkinSources.Add(skin);
sources.Add(skin);
break;
}
}
int lastDefaultSkinIndex = SkinSources.IndexOf(SkinSources.OfType<DefaultSkin>().LastOrDefault());
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)
SkinSources.Insert(lastDefaultSkinIndex, rulesetResourcesSkin);
sources.Insert(lastDefaultSkinIndex, rulesetResourcesSkin);
else
SkinSources.Add(rulesetResourcesSkin);
sources.Add(rulesetResourcesSkin);
foreach (var skin in sources)
AddSource(skin);
}
protected ISkin GetLegacyRulesetTransformedSkin(ISkin legacySkin)
@ -115,9 +111,6 @@ namespace osu.Game.Skinning
{
base.Dispose(isDisposing);
if (parentSource != null)
parentSource.SourceChanged -= OnSourceChanged;
rulesetResourcesSkin?.Dispose();
}
}

View File

@ -3,8 +3,6 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Audio.Sample;
@ -24,19 +22,8 @@ namespace osu.Game.Skinning
{
public event Action SourceChanged;
/// <summary>
/// Skins which should be exposed by this container, in order of lookup precedence.
/// </summary>
protected readonly BindableList<ISkin> SkinSources = new BindableList<ISkin>();
/// <summary>
/// A dictionary mapping each <see cref="ISkin"/> from the <see cref="SkinSources"/>
/// to one that performs the "allow lookup" checks before proceeding with a lookup.
/// </summary>
private readonly Dictionary<ISkin, DisableableSkinSource> disableableSkinSources = new Dictionary<ISkin, DisableableSkinSource>();
[CanBeNull]
private ISkinSource fallbackSource;
protected ISkinSource ParentSource { get; private set; }
/// <summary>
/// Whether falling back to parent <see cref="ISkinSource"/>s is allowed in this container.
@ -53,6 +40,11 @@ namespace osu.Game.Skinning
protected virtual bool AllowColourLookup => true;
/// <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)>();
/// <summary>
/// Constructs a new <see cref="SkinProvidingContainer"/> initialised with a single skin source.
/// </summary>
@ -60,87 +52,56 @@ namespace osu.Game.Skinning
: this()
{
if (skin != null)
SkinSources.Add(skin);
AddSource(skin);
}
/// <summary>
/// Constructs a new <see cref="SkinProvidingContainer"/> with no sources.
/// Implementations can add or change sources through the <see cref="SkinSources"/> list.
/// </summary>
protected SkinProvidingContainer()
{
RelativeSizeAxes = Axes.Both;
}
SkinSources.BindCollectionChanged(((_, args) =>
{
switch (args.Action)
{
case NotifyCollectionChangedAction.Add:
foreach (var skin in args.NewItems.Cast<ISkin>())
{
disableableSkinSources.Add(skin, new DisableableSkinSource(skin, this));
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
if (skin is ISkinSource source)
source.SourceChanged += OnSourceChanged;
}
ParentSource = dependencies.Get<ISkinSource>();
if (ParentSource != null)
ParentSource.SourceChanged += TriggerSourceChanged;
break;
dependencies.CacheAs<ISkinSource>(this);
case NotifyCollectionChangedAction.Reset:
case NotifyCollectionChangedAction.Remove:
foreach (var skin in args.OldItems.Cast<ISkin>())
{
disableableSkinSources.Remove(skin);
TriggerSourceChanged();
if (skin is ISkinSource source)
source.SourceChanged -= OnSourceChanged;
}
break;
case NotifyCollectionChangedAction.Replace:
foreach (var skin in args.OldItems.Cast<ISkin>())
{
disableableSkinSources.Remove(skin);
if (skin is ISkinSource source)
source.SourceChanged -= OnSourceChanged;
}
foreach (var skin in args.NewItems.Cast<ISkin>())
{
disableableSkinSources.Add(skin, new DisableableSkinSource(skin, this));
if (skin is ISkinSource source)
source.SourceChanged += OnSourceChanged;
}
break;
}
}), true);
return dependencies;
}
public ISkin FindProvider(Func<ISkin, bool> lookupFunction)
{
foreach (var skin in SkinSources)
foreach (var (skin, lookupWrapper) in skinSources)
{
if (lookupFunction(disableableSkinSources[skin]))
if (lookupFunction(lookupWrapper))
return skin;
}
return fallbackSource?.FindProvider(lookupFunction);
if (!AllowFallingBackToParent)
return null;
return ParentSource?.FindProvider(lookupFunction);
}
public IEnumerable<ISkin> AllSources
{
get
{
foreach (var skin in SkinSources)
yield return skin;
foreach (var i in skinSources)
yield return i.skin;
if (fallbackSource != null)
if (AllowFallingBackToParent && ParentSource != null)
{
foreach (var skin in fallbackSource.AllSources)
foreach (var skin in ParentSource.AllSources)
yield return skin;
}
}
@ -148,68 +109,110 @@ namespace osu.Game.Skinning
public Drawable GetDrawableComponent(ISkinComponent component)
{
foreach (var skin in SkinSources)
foreach (var (_, lookupWrapper) in skinSources)
{
Drawable sourceDrawable;
if ((sourceDrawable = disableableSkinSources[skin]?.GetDrawableComponent(component)) != null)
if ((sourceDrawable = lookupWrapper.GetDrawableComponent(component)) != null)
return sourceDrawable;
}
return fallbackSource?.GetDrawableComponent(component);
if (!AllowFallingBackToParent)
return null;
return ParentSource?.GetDrawableComponent(component);
}
public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT)
{
foreach (var skin in SkinSources)
foreach (var (_, lookupWrapper) in skinSources)
{
Texture sourceTexture;
if ((sourceTexture = disableableSkinSources[skin]?.GetTexture(componentName, wrapModeS, wrapModeT)) != null)
if ((sourceTexture = lookupWrapper.GetTexture(componentName, wrapModeS, wrapModeT)) != null)
return sourceTexture;
}
return fallbackSource?.GetTexture(componentName, wrapModeS, wrapModeT);
if (!AllowFallingBackToParent)
return null;
return ParentSource?.GetTexture(componentName, wrapModeS, wrapModeT);
}
public ISample GetSample(ISampleInfo sampleInfo)
{
foreach (var skin in SkinSources)
foreach (var (_, lookupWrapper) in skinSources)
{
ISample sourceSample;
if ((sourceSample = disableableSkinSources[skin]?.GetSample(sampleInfo)) != null)
if ((sourceSample = lookupWrapper.GetSample(sampleInfo)) != null)
return sourceSample;
}
return fallbackSource?.GetSample(sampleInfo);
if (!AllowFallingBackToParent)
return null;
return ParentSource?.GetSample(sampleInfo);
}
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup)
{
foreach (var skin in SkinSources)
foreach (var (_, lookupWrapper) in skinSources)
{
IBindable<TValue> bindable;
if ((bindable = disableableSkinSources[skin]?.GetConfig<TLookup, TValue>(lookup)) != null)
if ((bindable = lookupWrapper.GetConfig<TLookup, TValue>(lookup)) != null)
return bindable;
}
return fallbackSource?.GetConfig<TLookup, TValue>(lookup);
if (!AllowFallingBackToParent)
return null;
return ParentSource?.GetConfig<TLookup, TValue>(lookup);
}
protected virtual void OnSourceChanged() => SourceChanged?.Invoke();
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
/// <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)
{
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
skinSources.Add((skin, new DisableableSkinSource(skin, this)));
if (AllowFallingBackToParent)
{
fallbackSource = dependencies.Get<ISkinSource>();
if (fallbackSource != null)
fallbackSource.SourceChanged += OnSourceChanged;
}
if (skin is ISkinSource source)
source.SourceChanged += TriggerSourceChanged;
}
dependencies.CacheAs<ISkinSource>(this);
/// <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;
return dependencies;
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)
@ -219,11 +222,14 @@ namespace osu.Game.Skinning
base.Dispose(isDisposing);
if (fallbackSource != null)
fallbackSource.SourceChanged -= OnSourceChanged;
if (ParentSource != null)
ParentSource.SourceChanged -= TriggerSourceChanged;
foreach (var source in SkinSources.OfType<ISkinSource>())
source.SourceChanged -= OnSourceChanged;
foreach (var i in skinSources)
{
if (i.skin is ISkinSource source)
source.SourceChanged -= TriggerSourceChanged;
}
}
private class DisableableSkinSource : ISkin