mirror of
https://github.com/ppy/osu.git
synced 2024-12-05 10:33:22 +08:00
Compare commits
18 Commits
65186e8743
...
782a0494ea
Author | SHA1 | Date | |
---|---|---|---|
|
782a0494ea | ||
|
be05f2a1c2 | ||
|
6ff1dec7b2 | ||
|
e920cfa187 | ||
|
c12cb2582e | ||
|
164b809c89 | ||
|
f4e155bfa6 | ||
|
15a4726d68 | ||
|
01f3a2dd14 | ||
|
0e920a61da | ||
|
5ab9074f5d | ||
|
fcbfbd02fd | ||
|
c2215b10cf | ||
|
83ecfbd155 | ||
|
7df7727591 | ||
|
76f79ce083 | ||
|
f1b5686904 | ||
|
8dca69e3f9 |
@ -9,6 +9,7 @@ using System.Linq;
|
|||||||
using osu.Framework.Audio.Sample;
|
using osu.Framework.Audio.Sample;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Audio;
|
using osu.Game.Audio;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
@ -16,6 +17,7 @@ using osu.Game.Rulesets.Mania.Beatmaps;
|
|||||||
using osu.Game.Rulesets.Objects.Legacy;
|
using osu.Game.Rulesets.Objects.Legacy;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||||
{
|
{
|
||||||
@ -106,6 +108,20 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
|||||||
{
|
{
|
||||||
new LegacyManiaComboCounter(),
|
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;
|
return null;
|
||||||
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
194
osu.Game.Tests/Skins/TestSceneSkinDeserialisation.cs
Normal file
194
osu.Game.Tests/Skins/TestSceneSkinDeserialisation.cs
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
400
osu.Game.Tests/Skins/TestSceneSkinMigration.cs
Normal file
400
osu.Game.Tests/Skins/TestSceneSkinMigration.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -5,25 +5,64 @@ using System;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Audio;
|
||||||
|
using osu.Framework.Extensions;
|
||||||
|
using osu.Framework.Platform;
|
||||||
using osu.Framework.Screens;
|
using osu.Framework.Screens;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
using osu.Game.Online.API;
|
using osu.Game.Online.API;
|
||||||
using osu.Game.Online.API.Requests.Responses;
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
using osu.Game.Online.Rooms;
|
using osu.Game.Online.Rooms;
|
||||||
using osu.Game.Online.Rooms.RoomStatuses;
|
using osu.Game.Online.Rooms.RoomStatuses;
|
||||||
|
using osu.Game.Rulesets;
|
||||||
|
using osu.Game.Rulesets.Osu.Mods;
|
||||||
|
using osu.Game.Scoring;
|
||||||
using osu.Game.Screens.OnlinePlay.Playlists;
|
using osu.Game.Screens.OnlinePlay.Playlists;
|
||||||
|
using osu.Game.Tests.Resources;
|
||||||
using osu.Game.Tests.Visual.OnlinePlay;
|
using osu.Game.Tests.Visual.OnlinePlay;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Playlists
|
namespace osu.Game.Tests.Visual.Playlists
|
||||||
{
|
{
|
||||||
public partial class TestScenePlaylistsRoomSubScreen : OnlinePlayTestScene
|
public partial class TestScenePlaylistsRoomSubScreen : OnlinePlayTestScene
|
||||||
{
|
{
|
||||||
|
private const double track_length = 10000;
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private IAPIProvider api { get; set; } = null!;
|
private IAPIProvider api { get; set; } = null!;
|
||||||
|
|
||||||
protected new TestRoomManager RoomManager => (TestRoomManager)base.RoomManager;
|
protected new TestRoomManager RoomManager => (TestRoomManager)base.RoomManager;
|
||||||
|
|
||||||
|
private BeatmapManager beatmaps = null!;
|
||||||
|
private RulesetStore rulesets = null!;
|
||||||
|
private BeatmapSetInfo? importedSet;
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(GameHost host, AudioManager audio)
|
||||||
|
{
|
||||||
|
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
|
||||||
|
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default));
|
||||||
|
Dependencies.Cache(new ScoreManager(rulesets, () => beatmaps, LocalStorage, Realm, API));
|
||||||
|
Dependencies.Cache(Realm);
|
||||||
|
|
||||||
|
beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
|
||||||
|
|
||||||
|
Realm.Write(r =>
|
||||||
|
{
|
||||||
|
foreach (var set in r.All<BeatmapSetInfo>())
|
||||||
|
{
|
||||||
|
foreach (var b in set.Beatmaps)
|
||||||
|
{
|
||||||
|
// These will all have a virtual track length of 1000, see WorkingBeatmap.GetVirtualTrack().
|
||||||
|
b.Length = track_length - 1000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
importedSet = beatmaps.GetAllUsableBeatmapSets().First();
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestStatusUpdateOnEnter()
|
public void TestStatusUpdateOnEnter()
|
||||||
{
|
{
|
||||||
@ -69,5 +108,42 @@ namespace osu.Game.Tests.Visual.Playlists
|
|||||||
AddAssert("close button present", () => roomScreen.ChildrenOfType<DangerousRoundedButton>().Any());
|
AddAssert("close button present", () => roomScreen.ChildrenOfType<DangerousRoundedButton>().Any());
|
||||||
AddUntilStep("wait for close button to disappear", () => !roomScreen.ChildrenOfType<DangerousRoundedButton>().Any());
|
AddUntilStep("wait for close button to disappear", () => !roomScreen.ChildrenOfType<DangerousRoundedButton>().Any());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TestCase(120_000, true)] // Definitely enough time.
|
||||||
|
[TestCase(45_000, true)] // Enough time.
|
||||||
|
[TestCase(35_000, false)] // Not enough time to complete beatmap after lenience.
|
||||||
|
[TestCase(20_000, false)] // Not enough time.
|
||||||
|
[TestCase(5_000, false)] // Not enough time to complete beatmap before lenience.
|
||||||
|
[TestCase(37_500, true, 2)] // Enough time to complete beatmap after mods are applied.
|
||||||
|
public void TestReadyButtonEnablementPeriod(int offsetMs, bool enabled, double rate = 1)
|
||||||
|
{
|
||||||
|
Room room = null!;
|
||||||
|
PlaylistsRoomSubScreen roomScreen = null!;
|
||||||
|
|
||||||
|
AddStep("create room", () =>
|
||||||
|
{
|
||||||
|
RoomManager.AddRoom(room = new Room
|
||||||
|
{
|
||||||
|
Name = @"Test Room",
|
||||||
|
Host = api.LocalUser.Value,
|
||||||
|
Category = RoomCategory.Normal,
|
||||||
|
StartDate = DateTimeOffset.Now,
|
||||||
|
EndDate = DateTimeOffset.Now.AddMilliseconds(offsetMs),
|
||||||
|
Playlist =
|
||||||
|
[
|
||||||
|
new PlaylistItem(importedSet!.Beatmaps[0])
|
||||||
|
{
|
||||||
|
RequiredMods = rate == 1
|
||||||
|
? []
|
||||||
|
: [new APIMod(new OsuModDoubleTime { SpeedChange = { Value = rate } })]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("push screen", () => LoadScreen(roomScreen = new PlaylistsRoomSubScreen(room)));
|
||||||
|
AddUntilStep("wait for screen load", () => roomScreen.IsCurrentScreen());
|
||||||
|
AddUntilStep("ready button enabled", () => roomScreen.ChildrenOfType<PlaylistsReadyButton>().SingleOrDefault()?.Enabled.Value, () => Is.EqualTo(enabled));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
@ -10,7 +11,9 @@ using osu.Framework.Localisation;
|
|||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Online.Rooms;
|
using osu.Game.Online.Rooms;
|
||||||
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Screens.OnlinePlay.Components;
|
using osu.Game.Screens.OnlinePlay.Components;
|
||||||
|
using osu.Game.Utils;
|
||||||
|
|
||||||
namespace osu.Game.Screens.OnlinePlay.Playlists
|
namespace osu.Game.Screens.OnlinePlay.Playlists
|
||||||
{
|
{
|
||||||
@ -19,6 +22,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
|
|||||||
[Resolved]
|
[Resolved]
|
||||||
private IBindable<WorkingBeatmap> gameBeatmap { get; set; } = null!;
|
private IBindable<WorkingBeatmap> gameBeatmap { get; set; } = null!;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private IBindable<IReadOnlyList<Mod>> mods { get; set; } = null!;
|
||||||
|
|
||||||
private readonly Room room;
|
private readonly Room room;
|
||||||
|
|
||||||
public PlaylistsReadyButton(Room room)
|
public PlaylistsReadyButton(Room room)
|
||||||
@ -63,14 +69,14 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
|
|||||||
{
|
{
|
||||||
base.Update();
|
base.Update();
|
||||||
|
|
||||||
Enabled.Value = hasRemainingAttempts && enoughTimeLeft;
|
Enabled.Value = hasRemainingAttempts && enoughTimeLeft();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override LocalisableString TooltipText
|
public override LocalisableString TooltipText
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
if (!enoughTimeLeft)
|
if (!enoughTimeLeft())
|
||||||
return "No time left!";
|
return "No time left!";
|
||||||
|
|
||||||
if (!hasRemainingAttempts)
|
if (!hasRemainingAttempts)
|
||||||
@ -80,9 +86,17 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool enoughTimeLeft =>
|
private bool enoughTimeLeft()
|
||||||
// This should probably consider the length of the currently selected item, rather than a constant 30 seconds.
|
{
|
||||||
room.EndDate != null && DateTimeOffset.UtcNow.AddSeconds(30).AddMilliseconds(gameBeatmap.Value.Track.Length) < room.EndDate;
|
double rate = ModUtils.CalculateRateWithMods(mods.Value);
|
||||||
|
|
||||||
|
// We want to avoid users not being able to submit scores if they chose to not skip,
|
||||||
|
// so track length is chosen over playable length.
|
||||||
|
double trackLength = Math.Round(gameBeatmap.Value.Track.Length / rate);
|
||||||
|
|
||||||
|
// Additional 30 second delay added to account for load and/or submit time.
|
||||||
|
return room.EndDate != null && DateTimeOffset.UtcNow.AddSeconds(30).AddMilliseconds(trackLength) < room.EndDate;
|
||||||
|
}
|
||||||
|
|
||||||
protected override void Dispose(bool isDisposing)
|
protected override void Dispose(bool isDisposing)
|
||||||
{
|
{
|
||||||
|
@ -401,7 +401,6 @@ namespace osu.Game.Screens.Select
|
|||||||
if (beatmap == null || bpmLabelContainer == null)
|
if (beatmap == null || bpmLabelContainer == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// this doesn't consider mods which apply variable rates, yet.
|
|
||||||
double rate = ModUtils.CalculateRateWithMods(mods.Value);
|
double rate = ModUtils.CalculateRateWithMods(mods.Value);
|
||||||
|
|
||||||
int bpmMax = FormatUtils.RoundBPM(beatmap.ControlPointInfo.BPMMaximum, rate);
|
int bpmMax = FormatUtils.RoundBPM(beatmap.ControlPointInfo.BPMMaximum, rate);
|
||||||
|
@ -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 LegacyScoreCounter(),
|
||||||
new LegacyAccuracyCounter(),
|
new LegacyAccuracyCounter(),
|
||||||
new LegacySongProgress(),
|
new LegacySongProgress(),
|
||||||
new LegacyHealthDisplay(),
|
|
||||||
new BarHitErrorMeter(),
|
new BarHitErrorMeter(),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -23,6 +23,7 @@ using osu.Game.Database;
|
|||||||
using osu.Game.IO;
|
using osu.Game.IO;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
using osu.Game.Screens.Play.HUD;
|
using osu.Game.Screens.Play.HUD;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Skinning
|
namespace osu.Game.Skinning
|
||||||
{
|
{
|
||||||
@ -277,6 +278,10 @@ namespace osu.Game.Skinning
|
|||||||
|
|
||||||
private void applyMigration(SkinLayoutInfo layout, GlobalSkinnableContainers target, int version)
|
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)
|
switch (version)
|
||||||
{
|
{
|
||||||
case 1:
|
case 1:
|
||||||
@ -284,14 +289,13 @@ namespace osu.Game.Skinning
|
|||||||
// Combo counters were moved out of the global HUD components into per-ruleset.
|
// 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).
|
// 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 ||
|
if (target != GlobalSkinnableContainers.MainHUDComponents ||
|
||||||
!layout.TryGetDrawableInfo(null, out var globalHUDComponents) ||
|
!layout.TryGetDrawableInfo(null, out var globalHUDComponents))
|
||||||
resources == null)
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
var comboCounters = globalHUDComponents.Where(c =>
|
var comboCounters = globalHUDComponents.Where(c =>
|
||||||
c.Type.Name == nameof(LegacyDefaultComboCounter) ||
|
c.Type == typeof(LegacyDefaultComboCounter) ||
|
||||||
c.Type.Name == nameof(DefaultComboCounter) ||
|
c.Type == typeof(DefaultComboCounter) ||
|
||||||
c.Type.Name == nameof(ArgonComboCounter)).ToArray();
|
c.Type == typeof(ArgonComboCounter)).ToArray();
|
||||||
|
|
||||||
layout.Update(null, globalHUDComponents.Except(comboCounters).ToArray());
|
layout.Update(null, globalHUDComponents.Except(comboCounters).ToArray());
|
||||||
|
|
||||||
@ -307,6 +311,70 @@ namespace osu.Game.Skinning
|
|||||||
|
|
||||||
break;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,7 +39,10 @@ namespace osu.Game.Skinning
|
|||||||
|
|
||||||
public bool Protected { get; set; }
|
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)
|
var type = string.IsNullOrEmpty(InstantiationInfo)
|
||||||
// handle the case of skins imported before InstantiationInfo was added.
|
// 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.
|
// 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
|
// If we ever add more default skins in the future this will need some kind of proper migration rather than
|
||||||
// a single fallback.
|
// 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; }
|
public bool DeletePending { get; set; }
|
||||||
|
|
||||||
|
@ -26,9 +26,10 @@ namespace osu.Game.Skinning
|
|||||||
/// <list type="bullet">
|
/// <list type="bullet">
|
||||||
/// <item><description>0: Initial version of all skin layouts.</description></item>
|
/// <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>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>
|
/// </list>
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public const int LATEST_VERSION = 1;
|
public const int LATEST_VERSION = 2;
|
||||||
|
|
||||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
|
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
|
||||||
public int Version = LATEST_VERSION;
|
public int Version = LATEST_VERSION;
|
||||||
|
@ -286,6 +286,7 @@ namespace osu.Game.Utils
|
|||||||
{
|
{
|
||||||
double rate = 1;
|
double rate = 1;
|
||||||
|
|
||||||
|
// TODO: This doesn't consider mods which apply variable rates, yet.
|
||||||
foreach (var mod in mods.OfType<IApplicableToRate>())
|
foreach (var mod in mods.OfType<IApplicableToRate>())
|
||||||
rate = mod.ApplyToRate(0, rate);
|
rate = mod.ApplyToRate(0, rate);
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user