mirror of
https://github.com/ppy/osu.git
synced 2024-12-14 10:12:54 +08:00
Merge pull request #22673 from peppy/skin-per-ruleset-layouts
Add support for per-ruleset skin layouts
This commit is contained in:
commit
8818341047
@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
{
|
||||
public partial class DrawableOsuJudgement : DrawableJudgement
|
||||
{
|
||||
protected SkinnableLighting Lighting { get; private set; }
|
||||
internal SkinnableLighting Lighting { get; private set; }
|
||||
|
||||
[Resolved]
|
||||
private OsuConfigManager config { get; set; }
|
||||
|
@ -10,7 +10,7 @@ using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
{
|
||||
public partial class SkinnableLighting : SkinnableSprite
|
||||
internal partial class SkinnableLighting : SkinnableSprite
|
||||
{
|
||||
private DrawableHitObject targetObject;
|
||||
private JudgementResult targetResult;
|
||||
|
@ -66,9 +66,9 @@ namespace osu.Game.Tests.Skins
|
||||
{
|
||||
var skin = new TestSkin(new SkinInfo(), null, storage);
|
||||
|
||||
foreach (var target in skin.DrawableComponentInfo)
|
||||
foreach (var target in skin.LayoutInfos)
|
||||
{
|
||||
foreach (var info in target.Value)
|
||||
foreach (var info in target.Value.AllDrawables)
|
||||
instantiatedTypes.Add(info.Type);
|
||||
}
|
||||
}
|
||||
@ -87,8 +87,8 @@ namespace osu.Game.Tests.Skins
|
||||
{
|
||||
var skin = new TestSkin(new SkinInfo(), null, storage);
|
||||
|
||||
Assert.That(skin.DrawableComponentInfo, Has.Count.EqualTo(2));
|
||||
Assert.That(skin.DrawableComponentInfo[SkinComponentsContainerLookup.TargetArea.MainHUDComponents], Has.Length.EqualTo(9));
|
||||
Assert.That(skin.LayoutInfos, Has.Count.EqualTo(2));
|
||||
Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.ToArray(), Has.Length.EqualTo(9));
|
||||
}
|
||||
}
|
||||
|
||||
@ -100,11 +100,11 @@ namespace osu.Game.Tests.Skins
|
||||
{
|
||||
var skin = new TestSkin(new SkinInfo(), null, storage);
|
||||
|
||||
Assert.That(skin.DrawableComponentInfo, Has.Count.EqualTo(2));
|
||||
Assert.That(skin.DrawableComponentInfo[SkinComponentsContainerLookup.TargetArea.MainHUDComponents], Has.Length.EqualTo(6));
|
||||
Assert.That(skin.DrawableComponentInfo[SkinComponentsContainerLookup.TargetArea.SongSelect], Has.Length.EqualTo(1));
|
||||
Assert.That(skin.LayoutInfos, Has.Count.EqualTo(2));
|
||||
Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.ToArray(), Has.Length.EqualTo(6));
|
||||
Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.SongSelect].AllDrawables.ToArray(), Has.Length.EqualTo(1));
|
||||
|
||||
var skinnableInfo = skin.DrawableComponentInfo[SkinComponentsContainerLookup.TargetArea.SongSelect].First();
|
||||
var skinnableInfo = skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.SongSelect].AllDrawables.First();
|
||||
|
||||
Assert.That(skinnableInfo.Type, Is.EqualTo(typeof(SkinnableSprite)));
|
||||
Assert.That(skinnableInfo.Settings.First().Key, Is.EqualTo("sprite_name"));
|
||||
@ -115,10 +115,10 @@ namespace osu.Game.Tests.Skins
|
||||
using (var storage = new ZipArchiveReader(stream))
|
||||
{
|
||||
var skin = new TestSkin(new SkinInfo(), null, storage);
|
||||
Assert.That(skin.DrawableComponentInfo[SkinComponentsContainerLookup.TargetArea.MainHUDComponents], Has.Length.EqualTo(8));
|
||||
Assert.That(skin.DrawableComponentInfo[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].Select(i => i.Type), Contains.Item(typeof(UnstableRateCounter)));
|
||||
Assert.That(skin.DrawableComponentInfo[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].Select(i => i.Type), Contains.Item(typeof(ColourHitErrorMeter)));
|
||||
Assert.That(skin.DrawableComponentInfo[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].Select(i => i.Type), Contains.Item(typeof(LegacySongProgress)));
|
||||
Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.ToArray(), Has.Length.EqualTo(8));
|
||||
Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.Select(i => i.Type), Contains.Item(typeof(UnstableRateCounter)));
|
||||
Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.Select(i => i.Type), Contains.Item(typeof(ColourHitErrorMeter)));
|
||||
Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.Select(i => i.Type), Contains.Item(typeof(LegacySongProgress)));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -6,6 +6,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.EnumExtensions;
|
||||
@ -26,6 +27,7 @@ using osu.Game.Screens.Play.HUD.JudgementCounter;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Rulesets;
|
||||
|
||||
namespace osu.Game.Screens.Play
|
||||
{
|
||||
@ -100,20 +102,22 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
public HUDOverlay(DrawableRuleset drawableRuleset, IReadOnlyList<Mod> mods, bool alwaysShowLeaderboard = true)
|
||||
{
|
||||
Drawable rulesetComponents;
|
||||
|
||||
this.drawableRuleset = drawableRuleset;
|
||||
this.mods = mods;
|
||||
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
Children = new Drawable[]
|
||||
Children = new[]
|
||||
{
|
||||
CreateFailingLayer(),
|
||||
//Needs to be initialized before skinnable drawables.
|
||||
tally = new JudgementTally(),
|
||||
mainComponents = new MainComponentsContainer
|
||||
{
|
||||
AlwaysPresent = true,
|
||||
},
|
||||
mainComponents = new HUDComponentsContainer { AlwaysPresent = true, },
|
||||
rulesetComponents = drawableRuleset != null
|
||||
? new HUDComponentsContainer(drawableRuleset.Ruleset.RulesetInfo) { AlwaysPresent = true, }
|
||||
: Empty(),
|
||||
topRightElements = new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.TopRight,
|
||||
@ -155,7 +159,7 @@ namespace osu.Game.Screens.Play
|
||||
clicksPerSecondCalculator = new ClicksPerSecondCalculator(),
|
||||
};
|
||||
|
||||
hideTargets = new List<Drawable> { mainComponents, KeyCounter, topRightElements };
|
||||
hideTargets = new List<Drawable> { mainComponents, rulesetComponents, KeyCounter, topRightElements };
|
||||
|
||||
if (!alwaysShowLeaderboard)
|
||||
hideTargets.Add(LeaderboardFlow);
|
||||
@ -390,15 +394,15 @@ namespace osu.Game.Screens.Play
|
||||
}
|
||||
}
|
||||
|
||||
private partial class MainComponentsContainer : SkinComponentsContainer
|
||||
private partial class HUDComponentsContainer : SkinComponentsContainer
|
||||
{
|
||||
private Bindable<ScoringMode> scoringMode;
|
||||
|
||||
[Resolved]
|
||||
private OsuConfigManager config { get; set; }
|
||||
|
||||
public MainComponentsContainer()
|
||||
: base(new SkinComponentsContainerLookup(SkinComponentsContainerLookup.TargetArea.MainHUDComponents))
|
||||
public HUDComponentsContainer([CanBeNull] RulesetInfo ruleset = null)
|
||||
: base(new SkinComponentsContainerLookup(SkinComponentsContainerLookup.TargetArea.MainHUDComponents, ruleset))
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
}
|
||||
|
@ -91,6 +91,10 @@ namespace osu.Game.Skinning
|
||||
switch (lookup)
|
||||
{
|
||||
case SkinComponentsContainerLookup containerLookup:
|
||||
// Only handle global level defaults for now.
|
||||
if (containerLookup.Ruleset != null)
|
||||
return null;
|
||||
|
||||
switch (containerLookup.Target)
|
||||
{
|
||||
case SkinComponentsContainerLookup.TargetArea.SongSelect:
|
||||
|
@ -344,10 +344,14 @@ namespace osu.Game.Skinning
|
||||
switch (lookup)
|
||||
{
|
||||
case SkinComponentsContainerLookup containerLookup:
|
||||
// Only handle global level defaults for now.
|
||||
if (containerLookup.Ruleset != null)
|
||||
return null;
|
||||
|
||||
switch (containerLookup.Target)
|
||||
{
|
||||
case SkinComponentsContainerLookup.TargetArea.MainHUDComponents:
|
||||
var skinnableTargetWrapper = new DefaultSkinComponentsContainer(container =>
|
||||
return new DefaultSkinComponentsContainer(container =>
|
||||
{
|
||||
var score = container.OfType<LegacyScoreCounter>().FirstOrDefault();
|
||||
var accuracy = container.OfType<GameplayAccuracyCounter>().FirstOrDefault();
|
||||
@ -387,8 +391,6 @@ namespace osu.Game.Skinning
|
||||
new BarHitErrorMeter(),
|
||||
}
|
||||
};
|
||||
|
||||
return skinnableTargetWrapper;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
@ -11,6 +11,7 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Rulesets;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Skinning
|
||||
@ -100,13 +101,18 @@ namespace osu.Game.Skinning
|
||||
}
|
||||
}
|
||||
|
||||
public static Type[] GetAllAvailableDrawables()
|
||||
/// <summary>
|
||||
/// Retrieve all types available which support serialisation.
|
||||
/// </summary>
|
||||
/// <param name="ruleset">The ruleset to filter results to. If <c>null</c>, global components will be returned instead.</param>
|
||||
public static Type[] GetAllAvailableDrawables(RulesetInfo? ruleset = null)
|
||||
{
|
||||
return typeof(OsuGame).Assembly.GetTypes()
|
||||
.Where(t => !t.IsInterface && !t.IsAbstract)
|
||||
.Where(t => typeof(ISerialisableDrawable).IsAssignableFrom(t))
|
||||
.OrderBy(t => t.Name)
|
||||
.ToArray();
|
||||
return (ruleset?.CreateInstance().GetType() ?? typeof(OsuGame))
|
||||
.Assembly.GetTypes()
|
||||
.Where(t => !t.IsInterface && !t.IsAbstract && t.IsPublic)
|
||||
.Where(t => typeof(ISerialisableDrawable).IsAssignableFrom(t))
|
||||
.OrderBy(t => t.Name)
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -37,9 +37,10 @@ namespace osu.Game.Skinning
|
||||
|
||||
public SkinConfiguration Configuration { get; set; }
|
||||
|
||||
public IDictionary<SkinComponentsContainerLookup.TargetArea, SerialisedDrawableInfo[]> DrawableComponentInfo => drawableComponentInfo;
|
||||
public IDictionary<SkinComponentsContainerLookup.TargetArea, SkinLayoutInfo> LayoutInfos => layoutInfos;
|
||||
|
||||
private readonly Dictionary<SkinComponentsContainerLookup.TargetArea, SerialisedDrawableInfo[]> drawableComponentInfo = new Dictionary<SkinComponentsContainerLookup.TargetArea, SerialisedDrawableInfo[]>();
|
||||
private readonly Dictionary<SkinComponentsContainerLookup.TargetArea, SkinLayoutInfo> layoutInfos =
|
||||
new Dictionary<SkinComponentsContainerLookup.TargetArea, SkinLayoutInfo>();
|
||||
|
||||
public abstract ISample? GetSample(ISampleInfo sampleInfo);
|
||||
|
||||
@ -113,18 +114,41 @@ namespace osu.Game.Skinning
|
||||
{
|
||||
string jsonContent = Encoding.UTF8.GetString(bytes);
|
||||
|
||||
// handle namespace changes...
|
||||
SkinLayoutInfo? layoutInfo = null;
|
||||
|
||||
// can be removed 2023-01-31
|
||||
jsonContent = jsonContent.Replace(@"osu.Game.Screens.Play.SongProgress", @"osu.Game.Screens.Play.HUD.DefaultSongProgress");
|
||||
jsonContent = jsonContent.Replace(@"osu.Game.Screens.Play.HUD.LegacyComboCounter", @"osu.Game.Skinning.LegacyComboCounter");
|
||||
try
|
||||
{
|
||||
// First attempt to deserialise using the new SkinLayoutInfo format
|
||||
layoutInfo = JsonConvert.DeserializeObject<SkinLayoutInfo>(jsonContent);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
var deserializedContent = JsonConvert.DeserializeObject<IEnumerable<SerialisedDrawableInfo>>(jsonContent);
|
||||
// Of note, the migration code below runs on read of skins, but there's nothing to
|
||||
// force a rewrite after migration. Let's not remove these migration rules until we
|
||||
// have something in place to ensure we don't end up breaking skins of users that haven't
|
||||
// manually saved their skin since a change was implemented.
|
||||
|
||||
if (deserializedContent == null)
|
||||
continue;
|
||||
// If deserialisation using SkinLayoutInfo fails, attempt to deserialise using the old naked list.
|
||||
if (layoutInfo == null)
|
||||
{
|
||||
// handle namespace changes...
|
||||
jsonContent = jsonContent.Replace(@"osu.Game.Screens.Play.SongProgress", @"osu.Game.Screens.Play.HUD.DefaultSongProgress");
|
||||
jsonContent = jsonContent.Replace(@"osu.Game.Screens.Play.HUD.LegacyComboCounter", @"osu.Game.Skinning.LegacyComboCounter");
|
||||
|
||||
DrawableComponentInfo[skinnableTarget] = deserializedContent.ToArray();
|
||||
var deserializedContent = JsonConvert.DeserializeObject<IEnumerable<SerialisedDrawableInfo>>(jsonContent);
|
||||
|
||||
if (deserializedContent == null)
|
||||
continue;
|
||||
|
||||
layoutInfo = new SkinLayoutInfo();
|
||||
layoutInfo.Update(null, deserializedContent.ToArray());
|
||||
|
||||
Logger.Log($"Ferrying {deserializedContent.Count()} components in {skinnableTarget} to global section of new {nameof(SkinLayoutInfo)} format");
|
||||
}
|
||||
|
||||
LayoutInfos[skinnableTarget] = layoutInfo;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@ -145,7 +169,7 @@ namespace osu.Game.Skinning
|
||||
/// <param name="targetContainer">The target container to reset.</param>
|
||||
public void ResetDrawableTarget(SkinComponentsContainer targetContainer)
|
||||
{
|
||||
DrawableComponentInfo.Remove(targetContainer.Lookup.Target);
|
||||
LayoutInfos.Remove(targetContainer.Lookup.Target);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -154,7 +178,10 @@ namespace osu.Game.Skinning
|
||||
/// <param name="targetContainer">The target container to serialise to this skin.</param>
|
||||
public void UpdateDrawableTarget(SkinComponentsContainer targetContainer)
|
||||
{
|
||||
DrawableComponentInfo[targetContainer.Lookup.Target] = ((ISerialisableDrawableContainer)targetContainer).CreateSerialisedInfo().ToArray();
|
||||
if (!LayoutInfos.TryGetValue(targetContainer.Lookup.Target, out var layoutInfo))
|
||||
layoutInfos[targetContainer.Lookup.Target] = layoutInfo = new SkinLayoutInfo();
|
||||
|
||||
layoutInfo.Update(targetContainer.Lookup.Ruleset, ((ISerialisableDrawableContainer)targetContainer).CreateSerialisedInfo().ToArray());
|
||||
}
|
||||
|
||||
public virtual Drawable? GetDrawableComponent(ISkinComponentLookup lookup)
|
||||
@ -166,18 +193,16 @@ namespace osu.Game.Skinning
|
||||
return this.GetAnimation(sprite.LookupName, false, false);
|
||||
|
||||
case SkinComponentsContainerLookup containerLookup:
|
||||
if (!DrawableComponentInfo.TryGetValue(containerLookup.Target, out var skinnableInfo))
|
||||
return null;
|
||||
|
||||
var components = new List<Drawable>();
|
||||
|
||||
foreach (var i in skinnableInfo)
|
||||
components.Add(i.CreateInstance());
|
||||
// It is important to return null if the user has not configured this yet.
|
||||
// This allows skin transformers the opportunity to provide default components.
|
||||
if (!LayoutInfos.TryGetValue(containerLookup.Target, out var layoutInfo)) return null;
|
||||
if (!layoutInfo.TryGetDrawableInfo(containerLookup.Ruleset, out var drawableInfos)) return null;
|
||||
|
||||
return new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = components,
|
||||
ChildrenEnumerable = drawableInfos.Select(i => i.CreateInstance())
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -66,25 +66,20 @@ namespace osu.Game.Skinning
|
||||
components.Clear();
|
||||
ComponentsLoaded = false;
|
||||
|
||||
if (componentsContainer == null)
|
||||
return;
|
||||
|
||||
content = componentsContainer;
|
||||
content = componentsContainer ?? new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both
|
||||
};
|
||||
|
||||
cancellationSource?.Cancel();
|
||||
cancellationSource = null;
|
||||
|
||||
if (content != null)
|
||||
LoadComponentAsync(content, wrapper =>
|
||||
{
|
||||
LoadComponentAsync(content, wrapper =>
|
||||
{
|
||||
AddInternal(wrapper);
|
||||
components.AddRange(wrapper.Children.OfType<ISerialisableDrawable>());
|
||||
ComponentsLoaded = true;
|
||||
}, (cancellationSource = new CancellationTokenSource()).Token);
|
||||
}
|
||||
else
|
||||
AddInternal(wrapper);
|
||||
components.AddRange(wrapper.Children.OfType<ISerialisableDrawable>());
|
||||
ComponentsLoaded = true;
|
||||
}, (cancellationSource = new CancellationTokenSource()).Token);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="ISerialisableDrawableContainer"/>
|
||||
|
@ -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 osu.Game.Rulesets;
|
||||
|
||||
namespace osu.Game.Skinning
|
||||
{
|
||||
/// <summary>
|
||||
@ -13,9 +15,16 @@ namespace osu.Game.Skinning
|
||||
/// </summary>
|
||||
public readonly TargetArea Target;
|
||||
|
||||
public SkinComponentsContainerLookup(TargetArea target)
|
||||
/// <summary>
|
||||
/// The ruleset for which skin components should be returned.
|
||||
/// A <see langword="null"/> value means that returned components are global and should be applied for all rulesets.
|
||||
/// </summary>
|
||||
public readonly RulesetInfo? Ruleset;
|
||||
|
||||
public SkinComponentsContainerLookup(TargetArea target, RulesetInfo? ruleset = null)
|
||||
{
|
||||
Target = target;
|
||||
Ruleset = ruleset;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -201,7 +201,7 @@ namespace osu.Game.Skinning
|
||||
}
|
||||
|
||||
// Then serialise each of the drawable component groups into respective files.
|
||||
foreach (var drawableInfo in skin.DrawableComponentInfo)
|
||||
foreach (var drawableInfo in skin.LayoutInfos)
|
||||
{
|
||||
string json = JsonConvert.SerializeObject(drawableInfo.Value, new JsonSerializerSettings { Formatting = Formatting.Indented });
|
||||
|
||||
|
37
osu.Game/Skinning/SkinLayoutInfo.cs
Normal file
37
osu.Game/Skinning/SkinLayoutInfo.cs
Normal file
@ -0,0 +1,37 @@
|
||||
// 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.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using osu.Game.Rulesets;
|
||||
|
||||
namespace osu.Game.Skinning
|
||||
{
|
||||
/// <summary>
|
||||
/// A serialisable model describing layout of a <see cref="SkinComponentsContainer"/>.
|
||||
/// May contain multiple configurations for different rulesets, each of which should manifest their own <see cref="SkinComponentsContainer"/> as required.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class SkinLayoutInfo
|
||||
{
|
||||
private const string global_identifier = @"global";
|
||||
|
||||
[JsonIgnore]
|
||||
public IEnumerable<SerialisedDrawableInfo> AllDrawables => DrawableInfo.Values.SelectMany(v => v);
|
||||
|
||||
[JsonProperty]
|
||||
public Dictionary<string, SerialisedDrawableInfo[]> DrawableInfo { get; set; } = new Dictionary<string, SerialisedDrawableInfo[]>();
|
||||
|
||||
public bool TryGetDrawableInfo(RulesetInfo? ruleset, [NotNullWhen(true)] out SerialisedDrawableInfo[]? components) =>
|
||||
DrawableInfo.TryGetValue(ruleset?.ShortName ?? global_identifier, out components);
|
||||
|
||||
public void Reset(RulesetInfo? ruleset) =>
|
||||
DrawableInfo.Remove(ruleset?.ShortName ?? global_identifier);
|
||||
|
||||
public void Update(RulesetInfo? ruleset, SerialisedDrawableInfo[] components) =>
|
||||
DrawableInfo[ruleset?.ShortName ?? global_identifier] = components;
|
||||
}
|
||||
}
|
@ -69,6 +69,10 @@ namespace osu.Game.Skinning
|
||||
switch (lookup)
|
||||
{
|
||||
case SkinComponentsContainerLookup containerLookup:
|
||||
// Only handle global level defaults for now.
|
||||
if (containerLookup.Ruleset != null)
|
||||
return null;
|
||||
|
||||
switch (containerLookup.Target)
|
||||
{
|
||||
case SkinComponentsContainerLookup.TargetArea.SongSelect:
|
||||
|
Loading…
Reference in New Issue
Block a user