1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-05 03:22:55 +08:00
This commit is contained in:
Salman Alshamrani 2024-12-03 14:23:01 +09:00 committed by GitHub
commit 65186e8743
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 696 additions and 197 deletions

View File

@ -9,6 +9,7 @@ using System.Linq;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Audio;
using osu.Game.Beatmaps;
@ -16,6 +17,7 @@ using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Objects.Legacy;
using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Mania.Skinning.Legacy
{
@ -106,6 +108,20 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
{
new LegacyManiaComboCounter(),
};
case GlobalSkinnableContainers.Playfield:
return new Container
{
RelativeSizeAxes = Axes.Both,
Child = new LegacyHealthDisplay
{
Rotation = -90f,
Anchor = Anchor.BottomRight,
Origin = Anchor.TopLeft,
X = 1,
Scale = new Vector2(0.7f),
},
};
}
return null;

View File

@ -1,185 +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;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores;
using osu.Game.Audio;
using osu.Game.IO;
using osu.Game.IO.Archives;
using osu.Game.Screens.Menu;
using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Play.HUD.HitErrorMeters;
using osu.Game.Skinning;
using osu.Game.Skinning.Components;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Skins
{
/// <summary>
/// Test that the main components (which are serialised based on namespace/class name)
/// remain compatible with any changes.
/// </summary>
/// <remarks>
/// If this test breaks, check any naming or class structure changes.
/// Migration rules may need to be added to <see cref="Skin"/>.
/// </remarks>
[TestFixture]
public class SkinDeserialisationTest
{
private static readonly string[] available_skins =
{
// Covers song progress before namespace changes, and most other components.
"Archives/modified-default-20220723.osk",
"Archives/modified-classic-20220723.osk",
// Covers legacy song progress, UR counter, colour hit error metre.
"Archives/modified-classic-20220801.osk",
// Covers clicks/s counter
"Archives/modified-default-20220818.osk",
// Covers longest combo counter
"Archives/modified-default-20221012.osk",
// Covers Argon variant of song progress bar
"Archives/modified-argon-20221024.osk",
// Covers TextElement and BeatmapInfoDrawable
"Archives/modified-default-20221102.osk",
// Covers BPM counter.
"Archives/modified-default-20221205.osk",
// Covers judgement counter.
"Archives/modified-default-20230117.osk",
// Covers player avatar and flag.
"Archives/modified-argon-20230305.osk",
// Covers key counters
"Archives/modified-argon-pro-20230618.osk",
// Covers "Argon" health display
"Archives/modified-argon-pro-20231001.osk",
// Covers player name text component.
"Archives/modified-argon-20231106.osk",
// Covers "Argon" accuracy/score/combo counters, and wedges
"Archives/modified-argon-20231108.osk",
// Covers "Argon" performance points counter
"Archives/modified-argon-20240305.osk",
// Covers default rank display
"Archives/modified-default-20230809.osk",
// Covers legacy rank display
"Archives/modified-classic-20230809.osk",
// Covers legacy key counter
"Archives/modified-classic-20240724.osk"
};
/// <summary>
/// If this test fails, new test resources should be added to include new components.
/// </summary>
[Test]
public void TestSkinnableComponentsCoveredByDeserialisationTests()
{
HashSet<Type> instantiatedTypes = new HashSet<Type>();
foreach (string oskFile in available_skins)
{
using (var stream = TestResources.OpenResource(oskFile))
using (var storage = new ZipArchiveReader(stream))
{
var skin = new TestSkin(new SkinInfo(), null, storage);
foreach (var target in skin.LayoutInfos)
{
foreach (var info in target.Value.AllDrawables)
instantiatedTypes.Add(info.Type);
}
}
}
var editableTypes = SerialisedDrawableInfo.GetAllAvailableDrawables().Where(t => (Activator.CreateInstance(t) as ISerialisableDrawable)?.IsEditable == true);
Assert.That(instantiatedTypes, Is.EquivalentTo(editableTypes));
}
[Test]
public void TestDeserialiseModifiedDefault()
{
using (var stream = TestResources.OpenResource("Archives/modified-default-20220723.osk"))
using (var storage = new ZipArchiveReader(stream))
{
var skin = new TestSkin(new SkinInfo(), null, storage);
Assert.That(skin.LayoutInfos, Has.Count.EqualTo(2));
Assert.That(skin.LayoutInfos[GlobalSkinnableContainers.MainHUDComponents].AllDrawables.ToArray(), Has.Length.EqualTo(9));
}
}
[Test]
public void TestDeserialiseModifiedArgon()
{
using (var stream = TestResources.OpenResource("Archives/modified-argon-20231106.osk"))
using (var storage = new ZipArchiveReader(stream))
{
var skin = new TestSkin(new SkinInfo(), null, storage);
Assert.That(skin.LayoutInfos, Has.Count.EqualTo(2));
Assert.That(skin.LayoutInfos[GlobalSkinnableContainers.MainHUDComponents].AllDrawables.ToArray(), Has.Length.EqualTo(10));
Assert.That(skin.LayoutInfos[GlobalSkinnableContainers.MainHUDComponents].AllDrawables.Select(i => i.Type), Contains.Item(typeof(PlayerName)));
}
}
[Test]
public void TestDeserialiseInvalidDrawables()
{
using (var stream = TestResources.OpenResource("Archives/argon-invalid-drawable.osk"))
using (var storage = new ZipArchiveReader(stream))
{
var skin = new TestSkin(new SkinInfo(), null, storage);
Assert.That(skin.LayoutInfos.Any(kvp => kvp.Value.AllDrawables.Any(d => d.Type == typeof(StarFountain))), Is.False);
}
}
[Test]
public void TestDeserialiseModifiedClassic()
{
using (var stream = TestResources.OpenResource("Archives/modified-classic-20220723.osk"))
using (var storage = new ZipArchiveReader(stream))
{
var skin = new TestSkin(new SkinInfo(), null, storage);
Assert.That(skin.LayoutInfos, Has.Count.EqualTo(2));
Assert.That(skin.LayoutInfos[GlobalSkinnableContainers.MainHUDComponents].AllDrawables.ToArray(), Has.Length.EqualTo(6));
Assert.That(skin.LayoutInfos[GlobalSkinnableContainers.SongSelect].AllDrawables.ToArray(), Has.Length.EqualTo(1));
var skinnableInfo = skin.LayoutInfos[GlobalSkinnableContainers.SongSelect].AllDrawables.First();
Assert.That(skinnableInfo.Type, Is.EqualTo(typeof(SkinnableSprite)));
Assert.That(skinnableInfo.Settings.First().Key, Is.EqualTo("sprite_name"));
Assert.That(skinnableInfo.Settings.First().Value, Is.EqualTo("ppy_logo-2.png"));
}
using (var stream = TestResources.OpenResource("Archives/modified-classic-20220801.osk"))
using (var storage = new ZipArchiveReader(stream))
{
var skin = new TestSkin(new SkinInfo(), null, storage);
Assert.That(skin.LayoutInfos[GlobalSkinnableContainers.MainHUDComponents].AllDrawables.ToArray(), Has.Length.EqualTo(8));
Assert.That(skin.LayoutInfos[GlobalSkinnableContainers.MainHUDComponents].AllDrawables.Select(i => i.Type), Contains.Item(typeof(UnstableRateCounter)));
Assert.That(skin.LayoutInfos[GlobalSkinnableContainers.MainHUDComponents].AllDrawables.Select(i => i.Type), Contains.Item(typeof(ColourHitErrorMeter)));
Assert.That(skin.LayoutInfos[GlobalSkinnableContainers.MainHUDComponents].AllDrawables.Select(i => i.Type), Contains.Item(typeof(LegacySongProgress)));
}
}
private class TestSkin : Skin
{
public TestSkin(SkinInfo skin, IStorageResourceProvider? resources, IResourceStore<byte[]>? fallbackStore = null, string configurationFilename = "skin.ini")
: base(skin, resources, fallbackStore, configurationFilename)
{
}
public override Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => throw new NotImplementedException();
public override IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => throw new NotImplementedException();
public override ISample GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException();
}
}
}

View File

@ -0,0 +1,194 @@
// 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.IO;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Game.Database;
using osu.Game.Screens.Menu;
using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Play.HUD.HitErrorMeters;
using osu.Game.Skinning;
using osu.Game.Skinning.Components;
using osu.Game.Tests.Resources;
using osu.Game.Tests.Visual;
namespace osu.Game.Tests.Skins
{
/// <summary>
/// Test that the main components (which are serialised based on namespace/class name)
/// remain compatible with any changes.
/// </summary>
/// <remarks>
/// If this test breaks, check any naming or class structure changes.
/// Migration rules may need to be added to <see cref="Skin"/>.
/// </remarks>
public partial class TestSceneSkinDeserialisation : OsuTestScene
{
private static readonly string[] available_skins =
{
// Covers song progress before namespace changes, and most other components.
"Archives/modified-default-20220723.osk",
"Archives/modified-classic-20220723.osk",
// Covers legacy song progress, UR counter, colour hit error metre.
"Archives/modified-classic-20220801.osk",
// Covers clicks/s counter
"Archives/modified-default-20220818.osk",
// Covers longest combo counter
"Archives/modified-default-20221012.osk",
// Covers Argon variant of song progress bar
"Archives/modified-argon-20221024.osk",
// Covers TextElement and BeatmapInfoDrawable
"Archives/modified-default-20221102.osk",
// Covers BPM counter.
"Archives/modified-default-20221205.osk",
// Covers judgement counter.
"Archives/modified-default-20230117.osk",
// Covers player avatar and flag.
"Archives/modified-argon-20230305.osk",
// Covers key counters
"Archives/modified-argon-pro-20230618.osk",
// Covers "Argon" health display
"Archives/modified-argon-pro-20231001.osk",
// Covers player name text component.
"Archives/modified-argon-20231106.osk",
// Covers "Argon" accuracy/score/combo counters, and wedges
"Archives/modified-argon-20231108.osk",
// Covers "Argon" performance points counter
"Archives/modified-argon-20240305.osk",
// Covers default rank display
"Archives/modified-default-20230809.osk",
// Covers legacy rank display
"Archives/modified-classic-20230809.osk",
// Covers legacy key counter
"Archives/modified-classic-20240724.osk"
};
[Resolved]
private SkinManager skins { get; set; } = null!;
/// <summary>
/// If this test fails, new test resources should be added to include new components.
/// </summary>
[Test]
public void TestSkinnableComponentsCoveredByDeserialisationTests()
{
HashSet<Type> instantiatedTypes = new HashSet<Type>();
AddStep("load skin", () =>
{
foreach (string oskFile in available_skins)
{
var skin = importSkinFromArchives(oskFile);
foreach (var target in skin.LayoutInfos)
{
foreach (var info in target.Value.AllDrawables)
instantiatedTypes.Add(info.Type);
}
}
});
var existingTypes = SerialisedDrawableInfo.GetAllAvailableDrawables().Where(t => (Activator.CreateInstance(t) as ISerialisableDrawable)?.IsEditable == true);
AddAssert("all types available", () => instantiatedTypes, () => Is.EquivalentTo(existingTypes));
}
[Test]
public void TestDeserialiseModifiedDefault()
{
Skin skin = null!;
AddStep("load skin", () => skin = importSkinFromArchives("Archives/modified-default-20220723.osk"));
AddAssert("layouts count = 2", () => skin.LayoutInfos, () => Has.Count.EqualTo(2));
AddAssert("hud count = 12",
() => skin.LayoutInfos[GlobalSkinnableContainers.MainHUDComponents].AllDrawables.ToArray(),
() => Has.Length.EqualTo(12));
}
[Test]
public void TestDeserialiseModifiedArgon()
{
Skin skin = null!;
AddStep("load skin", () => skin = importSkinFromArchives("Archives/modified-argon-20231106.osk"));
AddAssert("layouts count = 2", () => skin.LayoutInfos, () => Has.Count.EqualTo(2));
AddAssert("hud count = 13",
() => skin.LayoutInfos[GlobalSkinnableContainers.MainHUDComponents].AllDrawables.ToArray(),
() => Has.Length.EqualTo(13));
AddAssert("hud contains player name",
() => skin.LayoutInfos[GlobalSkinnableContainers.MainHUDComponents].AllDrawables.Select(i => i.Type),
() => Does.Contain(typeof(PlayerName)));
}
[Test]
public void TestDeserialiseInvalidDrawables()
{
Skin skin = null!;
AddStep("load skin", () => skin = importSkinFromArchives("Archives/argon-invalid-drawable.osk"));
AddAssert("skin does not contain star fountain",
() => skin.LayoutInfos.SelectMany(kvp => kvp.Value.AllDrawables).Select(d => d.Type),
() => Does.Not.Contain(typeof(StarFountain)));
}
[Test]
public void TestDeserialiseModifiedClassic()
{
Skin skin = null!;
AddStep("load skin", () => skin = importSkinFromArchives("Archives/modified-classic-20220723.osk"));
AddAssert("layouts count = 2", () => skin.LayoutInfos, () => Has.Count.EqualTo(2));
AddAssert("hud count = 11",
() => skin.LayoutInfos[GlobalSkinnableContainers.MainHUDComponents].AllDrawables.ToArray(),
() => Has.Length.EqualTo(11));
AddAssert("song select count = 1",
() => skin.LayoutInfos[GlobalSkinnableContainers.SongSelect].AllDrawables.ToArray(),
() => Has.Length.EqualTo(1));
AddAssert("song select component correct", () =>
{
var skinnableInfo = skin.LayoutInfos[GlobalSkinnableContainers.SongSelect].AllDrawables.First();
Assert.That(skinnableInfo.Type, Is.EqualTo(typeof(SkinnableSprite)));
Assert.That(skinnableInfo.Settings.First().Key, Is.EqualTo("sprite_name"));
Assert.That(skinnableInfo.Settings.First().Value, Is.EqualTo("ppy_logo-2.png"));
return true;
});
AddStep("load another skin", () => skin = importSkinFromArchives("Archives/modified-classic-20220801.osk"));
AddAssert("hud count = 13",
() => skin.LayoutInfos[GlobalSkinnableContainers.MainHUDComponents].AllDrawables.ToArray(),
() => Has.Length.EqualTo(13));
AddAssert("hud contains ur counter",
() => skin.LayoutInfos[GlobalSkinnableContainers.MainHUDComponents].AllDrawables.Select(i => i.Type),
() => Does.Contain(typeof(UnstableRateCounter)));
AddAssert("hud contains colour hit error",
() => skin.LayoutInfos[GlobalSkinnableContainers.MainHUDComponents].AllDrawables.Select(i => i.Type),
() => Does.Contain(typeof(ColourHitErrorMeter)));
AddAssert("hud contains legacy song progress",
() => skin.LayoutInfos[GlobalSkinnableContainers.MainHUDComponents].AllDrawables.Select(i => i.Type),
() => Does.Contain(typeof(LegacySongProgress)));
}
private Skin importSkinFromArchives(string filename)
{
var imported = skins.Import(new ImportTask(TestResources.OpenResource(filename), Path.GetFileNameWithoutExtension(filename))).GetResultSafely();
return imported.PerformRead(skinInfo => skins.GetSkin(skinInfo));
}
}
}

View File

@ -0,0 +1,400 @@
// 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 System.Text;
using Newtonsoft.Json;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Rendering.Dummy;
using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores;
using osu.Framework.Testing;
using osu.Game.Database;
using osu.Game.Extensions;
using osu.Game.IO;
using osu.Game.Models;
using osu.Game.Rulesets;
using osu.Game.Skinning;
using osu.Game.Skinning.Components;
using osu.Game.Tests.Visual;
namespace osu.Game.Tests.Skins
{
[HeadlessTest]
public partial class TestSceneSkinMigration : OsuTestScene
{
[Resolved]
private RulesetStore rulesets { get; set; } = null!;
[Test]
public void TestEmptyConfiguration()
{
LegacySkin skin = null!;
AddStep("load skin with empty configuration", () => skin = loadSkin<LegacySkin>());
AddAssert("skin has no configuration", () => !skin.LayoutInfos.Any());
}
[Test]
public void TestSomeConfiguration()
{
LegacySkin skin = null!;
AddStep("load skin", () =>
{
skin = loadSkin<LegacySkin>(new Dictionary<GlobalSkinnableContainers, SkinLayoutInfo>
{
{ GlobalSkinnableContainers.MainHUDComponents, createLayout(SkinLayoutInfo.LATEST_VERSION, [nameof(LegacyHealthDisplay)]) },
});
});
AddAssert("skin has correct configuration", () =>
{
return skin.LayoutInfos.Single().Value.DrawableInfo["global"].Select(d => d.Type.Name).SequenceEqual([nameof(BigBlackBox), nameof(LegacyHealthDisplay)]) &&
skin.LayoutInfos.Single().Value.DrawableInfo.Where(d => d.Key != @"global")
.All(d => d.Value.Single().Type.Name == nameof(BigBlackBox));
});
}
#region Version 1
[Test]
public void TestMigration_1()
{
LegacySkin skin = null!;
AddStep("load skin", () =>
{
skin = loadSkin<LegacySkin>(new Dictionary<GlobalSkinnableContainers, SkinLayoutInfo>
{
{
GlobalSkinnableContainers.MainHUDComponents,
createLayout(0, [nameof(LegacyDefaultComboCounter)])
},
});
});
AddAssert("combo counter removed from global", () =>
{
var dict = skin.LayoutInfos[GlobalSkinnableContainers.MainHUDComponents].DrawableInfo;
return dict.Single(kvp => kvp.Key == @"global").Value.Single().Type.Name == nameof(BigBlackBox);
});
AddAssert("combo counter moved to each ruleset", () =>
{
var dict = skin.LayoutInfos[GlobalSkinnableContainers.MainHUDComponents].DrawableInfo;
return dict.Where(kvp => kvp.Key != @"global").All(kvp => kvp.Value.Select(d => d.Type.Name).SequenceEqual([nameof(BigBlackBox), nameof(LegacyDefaultComboCounter)]));
});
}
[Test]
public void TestMigration_1_NoComboCounter()
{
LegacySkin skin = null!;
AddStep("load skin", () =>
{
skin = loadSkin<LegacySkin>(new Dictionary<GlobalSkinnableContainers, SkinLayoutInfo>
{
{
GlobalSkinnableContainers.MainHUDComponents,
createLayout(0)
},
});
});
AddAssert("nothing removed from global", () =>
{
var dict = skin.LayoutInfos[GlobalSkinnableContainers.MainHUDComponents].DrawableInfo;
return dict.Single(kvp => kvp.Key == @"global").Value.Single().Type.Name == nameof(BigBlackBox);
});
AddAssert("no combo counter added", () =>
{
var dict = skin.LayoutInfos[GlobalSkinnableContainers.MainHUDComponents].DrawableInfo;
return dict.Where(kvp => kvp.Key != @"global").All(kvp => kvp.Value.Select(d => d.Type.Name).SequenceEqual([nameof(BigBlackBox)]));
});
}
#endregion
#region Version 2
[Test]
public void TestMigration_2()
{
LegacySkin skin = null!;
AddStep("load skin", () =>
{
skin = loadSkin<LegacySkin>(new Dictionary<GlobalSkinnableContainers, SkinLayoutInfo>
{
{
GlobalSkinnableContainers.MainHUDComponents,
createLayout(1, [nameof(LegacyHealthDisplay)])
},
{
GlobalSkinnableContainers.Playfield,
createLayout(1)
}
});
});
// HUD
AddAssert("health display removed from global HUD", () =>
{
var dict = skin.LayoutInfos[GlobalSkinnableContainers.MainHUDComponents].DrawableInfo;
return dict.Single(kvp => kvp.Key == @"global").Value.Single().Type.Name == nameof(BigBlackBox);
});
AddAssert("health display moved to each ruleset except mania", () =>
{
var dict = skin.LayoutInfos[GlobalSkinnableContainers.MainHUDComponents].DrawableInfo.ToArray();
dict = dict.Where(kvp => kvp.Key != @"global" && kvp.Key != @"mania").ToArray();
return dict.All(kvp => kvp.Value.Select(d => d.Type.Name).SequenceEqual([nameof(BigBlackBox), nameof(LegacyHealthDisplay)])) &&
dict.All(kvp => kvp.Value.Single(d => d.Type.Name == nameof(LegacyHealthDisplay)).Rotation == 0f);
});
AddAssert("no health display in mania HUD", () =>
{
var dict = skin.LayoutInfos[GlobalSkinnableContainers.MainHUDComponents].DrawableInfo;
return dict.Single(kvp => kvp.Key == @"mania").Value.Single().Type.Name == nameof(BigBlackBox);
});
// Playfield
AddAssert("health display in mania moved to playfield", () =>
{
var dict = skin.LayoutInfos[GlobalSkinnableContainers.Playfield].DrawableInfo;
return dict.Single(kvp => kvp.Key == @"mania").Value.Select(d => d.Type.Name).SequenceEqual([nameof(BigBlackBox), nameof(LegacyHealthDisplay)]) &&
dict.Single(kvp => kvp.Key == @"mania").Value.Single(d => d.Type.Name == nameof(LegacyHealthDisplay)).Rotation == -90f;
});
AddAssert("rest is unaffected", () =>
{
var dict = skin.LayoutInfos[GlobalSkinnableContainers.Playfield].DrawableInfo;
return dict.Where(kvp => kvp.Key != @"mania").All(kvp => kvp.Value.Single().Type.Name == nameof(BigBlackBox));
});
}
[Test]
public void TestMigration_2_NonLegacySkin()
{
ArgonSkin skin = null!;
AddStep("load argon skin", () =>
{
skin = loadSkin<ArgonSkin>(new Dictionary<GlobalSkinnableContainers, SkinLayoutInfo>
{
{
GlobalSkinnableContainers.MainHUDComponents,
createLayout(1, [nameof(LegacyHealthDisplay)])
},
{
GlobalSkinnableContainers.Playfield,
createLayout(1)
}
});
});
// HUD
AddAssert("health display still in global HUD", () =>
{
var dict = skin.LayoutInfos[GlobalSkinnableContainers.MainHUDComponents].DrawableInfo;
return dict.Single(kvp => kvp.Key == @"global").Value.Select(d => d.Type.Name).SequenceEqual([nameof(BigBlackBox), nameof(LegacyHealthDisplay)]) &&
dict.Single(kvp => kvp.Key == @"global").Value.Single(d => d.Type.Name == nameof(LegacyHealthDisplay)).Rotation == 0f;
});
AddAssert("ruleset HUDs unaffected", () =>
{
var dict = skin.LayoutInfos[GlobalSkinnableContainers.MainHUDComponents].DrawableInfo.ToArray();
return dict.Where(kvp => kvp.Key != @"global").All(kvp => kvp.Value.Single().Type.Name == nameof(BigBlackBox));
});
// Playfield
AddAssert("playfield unaffected", () =>
{
var dict = skin.LayoutInfos[GlobalSkinnableContainers.Playfield].DrawableInfo.ToArray();
return dict.All(kvp => kvp.Value.Single().Type.Name == nameof(BigBlackBox));
});
}
[Test]
public void TestMigration_2_NoHUD()
{
LegacySkin skin = null!;
AddStep("load skin", () =>
{
skin = loadSkin<LegacySkin>(new Dictionary<GlobalSkinnableContainers, SkinLayoutInfo>
{
{
GlobalSkinnableContainers.Playfield,
createLayout(1)
},
});
});
// In this case, we must add a health display to the Playfield target,
// otherwise on mania the user will not see a health display anymore.
// HUD
AddAssert("HUD not configured", () => !skin.LayoutInfos.ContainsKey(GlobalSkinnableContainers.MainHUDComponents));
// Playfield
AddAssert("health display in mania moved to playfield", () =>
{
var dict = skin.LayoutInfos[GlobalSkinnableContainers.Playfield].DrawableInfo;
return dict.Single(kvp => kvp.Key == @"mania").Value.Select(d => d.Type.Name).SequenceEqual([nameof(BigBlackBox), nameof(LegacyHealthDisplay)]) &&
dict.Single(kvp => kvp.Key == @"mania").Value.Single(d => d.Type.Name == nameof(LegacyHealthDisplay)).Rotation == -90f;
});
AddAssert("rest is unaffected", () =>
{
var dict = skin.LayoutInfos[GlobalSkinnableContainers.Playfield].DrawableInfo;
return dict.Where(kvp => kvp.Key != @"mania").All(d => d.Value.Single().Type.Name == nameof(BigBlackBox));
});
}
[Test]
public void TestMigration_2_NoPlayfield()
{
LegacySkin skin = null!;
AddStep("load skin", () =>
{
skin = loadSkin<LegacySkin>(new Dictionary<GlobalSkinnableContainers, SkinLayoutInfo>
{
{
GlobalSkinnableContainers.MainHUDComponents,
createLayout(1, [nameof(LegacyHealthDisplay)])
}
});
});
// HUD
AddAssert("health display removed from global HUD", () =>
{
var dict = skin.LayoutInfos[GlobalSkinnableContainers.MainHUDComponents].DrawableInfo;
return dict.Single(kvp => kvp.Key == @"global").Value.Single().Type.Name == nameof(BigBlackBox);
});
AddAssert("health display moved to each ruleset except mania", () =>
{
var dict = skin.LayoutInfos[GlobalSkinnableContainers.MainHUDComponents].DrawableInfo.ToArray();
dict = dict.Where(kvp => kvp.Key != @"global" && kvp.Key != @"mania").ToArray();
return dict.All(kvp => kvp.Value.Select(d => d.Type.Name).SequenceEqual([nameof(BigBlackBox), nameof(LegacyHealthDisplay)])) &&
dict.All(kvp => kvp.Value.Single(d => d.Type.Name == nameof(LegacyHealthDisplay)).Rotation == 0f);
});
AddAssert("no health display in mania HUD", () =>
{
var dict = skin.LayoutInfos[GlobalSkinnableContainers.MainHUDComponents].DrawableInfo;
return dict.Single(kvp => kvp.Key == @"mania").Value.Single().Type.Name == nameof(BigBlackBox);
});
// Playfield
AddAssert("playfield not configured", () => !skin.LayoutInfos.ContainsKey(GlobalSkinnableContainers.Playfield));
}
#endregion
private SkinLayoutInfo createLayout(int version, string[]? globalComponents = null, string? ruleset = null, string[]? rulesetComponents = null)
{
var info = new SkinLayoutInfo { Version = version };
if (globalComponents != null)
info.DrawableInfo.Add(@"global", globalComponents.Select(c => resolveComponent(c).CreateSerialisedInfo()).ToArray());
if (ruleset != null && rulesetComponents != null)
info.DrawableInfo.Add(ruleset, rulesetComponents.Select(c => resolveComponent(c).CreateSerialisedInfo()).ToArray());
// add random drawable to ensure nothing is incorrectly discarded
foreach (string key in rulesets.AvailableRulesets.Select(r => r.ShortName).Prepend(@"global"))
{
if (!info.DrawableInfo.TryGetValue(key, out var drawables))
info.DrawableInfo.Add(key, drawables = Array.Empty<SerialisedDrawableInfo>());
info.DrawableInfo[key] = drawables.Prepend(new BigBlackBox().CreateSerialisedInfo()).ToArray();
}
return info;
}
private Drawable resolveComponent(string name, string? ruleset = null)
{
var drawables = SerialisedDrawableInfo.GetAllAvailableDrawables();
if (ruleset != null)
drawables = drawables.Concat(SerialisedDrawableInfo.GetAllAvailableDrawables(rulesets.GetRuleset(ruleset))).ToArray();
return (Drawable)Activator.CreateInstance(drawables.Single(d => d.Name == name))!;
}
private T loadSkin<T>(IDictionary<GlobalSkinnableContainers, SkinLayoutInfo>? layout = null)
where T : Skin
{
var info = new TestSkinInfo(typeof(T).GetInvariantInstantiationInfo(), layout);
return (T)info.CreateInstance(new TestStorageResourceProvider(layout, info.Files, Realm));
}
private class TestSkinInfo : SkinInfo
{
public override IList<RealmNamedFileUsage> Files { get; } = new List<RealmNamedFileUsage>();
public TestSkinInfo(string instantiationInfo, IDictionary<GlobalSkinnableContainers, SkinLayoutInfo>? layout)
: base("test skin", "me", instantiationInfo)
{
if (layout != null)
{
foreach (var kvp in layout)
Files.Add(new RealmNamedFileUsage(new RealmFile { Hash = Guid.NewGuid().ToString().ComputeMD5Hash() }, kvp.Key + ".json"));
}
}
}
private class TestStorageResourceProvider : IStorageResourceProvider
{
public IRenderer Renderer { get; } = new DummyRenderer();
public IResourceStore<byte[]> Resources { get; } = new ResourceStore<byte[]>();
public IResourceStore<TextureUpload>? CreateTextureLoaderStore(IResourceStore<byte[]> underlyingStore) => null;
public AudioManager? AudioManager => null;
public IResourceStore<byte[]> Files { get; }
public RealmAccess RealmAccess { get; }
public TestStorageResourceProvider(IDictionary<GlobalSkinnableContainers, SkinLayoutInfo>? layout, IList<RealmNamedFileUsage> files, RealmAccess realm)
{
Files = new TestResourceStore(layout, files);
RealmAccess = realm;
}
private class TestResourceStore : ResourceStore<byte[]>
{
private readonly IDictionary<GlobalSkinnableContainers, SkinLayoutInfo>? layout;
private readonly IList<RealmNamedFileUsage> files;
public TestResourceStore(IDictionary<GlobalSkinnableContainers, SkinLayoutInfo>? layout, IList<RealmNamedFileUsage> files)
{
this.layout = layout;
this.files = files;
}
public override byte[] Get(string name)
{
string? filename = files.SingleOrDefault(f => f.File.GetStoragePath() == name)?.Filename;
if (filename == null || layout == null)
return base.Get(name);
if (!Enum.TryParse<GlobalSkinnableContainers>(filename.Replace(@".json", string.Empty), out var type) ||
!layout.TryGetValue(type, out var info))
return base.Get(name);
string json = JsonConvert.SerializeObject(info, new JsonSerializerSettings { Formatting = Formatting.Indented });
return Encoding.UTF8.GetBytes(json);
}
}
}
}
}

View File

@ -376,7 +376,8 @@ namespace osu.Game.Skinning
}
})
{
new LegacyDefaultComboCounter()
new LegacyDefaultComboCounter(),
new LegacyHealthDisplay(),
};
}
@ -415,7 +416,6 @@ namespace osu.Game.Skinning
new LegacyScoreCounter(),
new LegacyAccuracyCounter(),
new LegacySongProgress(),
new LegacyHealthDisplay(),
new BarHitErrorMeter(),
}
};

View File

@ -23,6 +23,7 @@ using osu.Game.Database;
using osu.Game.IO;
using osu.Game.Rulesets;
using osu.Game.Screens.Play.HUD;
using osuTK;
namespace osu.Game.Skinning
{
@ -277,6 +278,10 @@ namespace osu.Game.Skinning
private void applyMigration(SkinLayoutInfo layout, GlobalSkinnableContainers target, int version)
{
Debug.Assert(resources != null);
bool isLegacySkin = SkinInfo.PerformRead(s => s.GetInstanceType().IsAssignableTo(typeof(LegacySkin)));
switch (version)
{
case 1:
@ -284,14 +289,13 @@ namespace osu.Game.Skinning
// Combo counters were moved out of the global HUD components into per-ruleset.
// This is to allow some rulesets to customise further (ie. mania and catch moving the combo to within their play area).
if (target != GlobalSkinnableContainers.MainHUDComponents ||
!layout.TryGetDrawableInfo(null, out var globalHUDComponents) ||
resources == null)
!layout.TryGetDrawableInfo(null, out var globalHUDComponents))
break;
var comboCounters = globalHUDComponents.Where(c =>
c.Type.Name == nameof(LegacyDefaultComboCounter) ||
c.Type.Name == nameof(DefaultComboCounter) ||
c.Type.Name == nameof(ArgonComboCounter)).ToArray();
c.Type == typeof(LegacyDefaultComboCounter) ||
c.Type == typeof(DefaultComboCounter) ||
c.Type == typeof(ArgonComboCounter)).ToArray();
layout.Update(null, globalHUDComponents.Except(comboCounters).ToArray());
@ -307,6 +311,70 @@ namespace osu.Game.Skinning
break;
}
case 2:
{
// Health displays are moved out of the global HUD components and into per-ruleset,
// except for osu!mania, wherein the health display is moved to the Playfield target.
switch (target)
{
case GlobalSkinnableContainers.MainHUDComponents:
if (!isLegacySkin || !layout.TryGetDrawableInfo(null, out var globalHUDComponents))
break;
var healthDisplays = globalHUDComponents.Where(c => c.Type == typeof(LegacyHealthDisplay)).ToArray();
layout.Update(null, globalHUDComponents.Except(healthDisplays).ToArray());
resources.RealmAccess.Run(r =>
{
foreach (var ruleset in r.All<RulesetInfo>())
{
// for mania, the health display is moved from MainHUDComponents to Playfield.
if (ruleset.ShortName == @"mania")
continue;
layout.Update(ruleset, layout.TryGetDrawableInfo(ruleset, out var rulesetHUDComponents)
? rulesetHUDComponents.Concat(healthDisplays).ToArray()
: healthDisplays);
}
});
break;
case GlobalSkinnableContainers.Playfield:
if (!isLegacySkin)
{
// One may argue that if a LegacyHealthDisplay exists in a non-legacy skin,
// then it should be swapped with the mania variant similar to legacy skins.
// This is not simple to achieve as we have to be aware of the presence of
// the health display in the HUD layout while migrating the Playfield layout,
// which is impossible with the current structure of skin layout migration.
// Instead, don't touch any non-legacy skin and call it a day.
break;
}
resources.RealmAccess.Run(r =>
{
var maniaRuleset = r.Find<RulesetInfo>(@"mania");
if (!layout.TryGetDrawableInfo(maniaRuleset, out var maniaPlayfieldComponents))
maniaPlayfieldComponents = Array.Empty<SerialisedDrawableInfo>();
layout.Update(maniaRuleset, maniaPlayfieldComponents.Append(new LegacyHealthDisplay
{
Rotation = -90f,
Anchor = Anchor.BottomRight,
Origin = Anchor.TopLeft,
X = 1,
Scale = new Vector2(0.7f),
}.CreateSerialisedInfo()).ToArray());
});
break;
}
break;
}
}
}

View File

@ -39,7 +39,10 @@ namespace osu.Game.Skinning
public bool Protected { get; set; }
public virtual Skin CreateInstance(IStorageResourceProvider resources)
/// <summary>
/// Returns the specific subtype of <see cref="Skin"/> that will be constructed on calling <see cref="CreateInstance"/>.
/// </summary>
internal Type GetInstanceType()
{
var type = string.IsNullOrEmpty(InstantiationInfo)
// handle the case of skins imported before InstantiationInfo was added.
@ -52,13 +55,15 @@ namespace osu.Game.Skinning
// for user modified skins. This aims to amicably handle that.
// If we ever add more default skins in the future this will need some kind of proper migration rather than
// a single fallback.
return new TrianglesSkin(this, resources);
return typeof(TrianglesSkin);
}
return (Skin)Activator.CreateInstance(type, this, resources)!;
return type;
}
public IList<RealmNamedFileUsage> Files { get; } = null!;
public virtual Skin CreateInstance(IStorageResourceProvider resources) => (Skin)Activator.CreateInstance(GetInstanceType(), this, resources)!;
public virtual IList<RealmNamedFileUsage> Files { get; } = null!;
public bool DeletePending { get; set; }

View File

@ -26,9 +26,10 @@ namespace osu.Game.Skinning
/// <list type="bullet">
/// <item><description>0: Initial version of all skin layouts.</description></item>
/// <item><description>1: Moves existing combo counters from global to per-ruleset HUD targets.</description></item>
/// <item><description>2: Moves existing legacy health bars from global to per-ruleset HUD targets, and to playfield target on mania.</description></item>
/// </list>
/// </remarks>
public const int LATEST_VERSION = 1;
public const int LATEST_VERSION = 2;
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
public int Version = LATEST_VERSION;