1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-05 10:33:22 +08:00

Add migration logic for legacy health displays

This commit is contained in:
Salman Alshamrani 2024-11-25 23:24:19 -05:00
parent c2215b10cf
commit fcbfbd02fd
5 changed files with 259 additions and 17 deletions

View File

@ -44,7 +44,7 @@ namespace osu.Game.Tests.Visual.Skinning
{ {
LegacySkin skin = null!; LegacySkin skin = null!;
AddStep("load skin with some configuration", () => AddStep("load skin", () =>
{ {
skin = loadSkin<LegacySkin>(new Dictionary<GlobalSkinnableContainers, SkinLayoutInfo> skin = loadSkin<LegacySkin>(new Dictionary<GlobalSkinnableContainers, SkinLayoutInfo>
{ {
@ -120,16 +120,195 @@ namespace osu.Game.Tests.Visual.Skinning
#endregion #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)
}
});
});
// 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.
// 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) private SkinLayoutInfo createLayout(int version, string[]? globalComponents = null, string? ruleset = null, string[]? rulesetComponents = null)
{ {
var info = new SkinLayoutInfo var info = new SkinLayoutInfo { Version = version };
{
Version = version, if (globalComponents != null)
DrawableInfo = info.DrawableInfo.Add(@"global", globalComponents.Select(c => resolveComponent(c).CreateSerialisedInfo()).ToArray());
{
{ "global", globalComponents?.Select(c => resolveComponent(c).CreateSerialisedInfo()).ToArray() ?? Array.Empty<SerialisedDrawableInfo>() },
}
};
if (ruleset != null && rulesetComponents != null) if (ruleset != null && rulesetComponents != null)
info.DrawableInfo.Add(ruleset, rulesetComponents.Select(c => resolveComponent(c).CreateSerialisedInfo()).ToArray()); info.DrawableInfo.Add(ruleset, rulesetComponents.Select(c => resolveComponent(c).CreateSerialisedInfo()).ToArray());

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 LegacyScoreCounter(),
new LegacyAccuracyCounter(), new LegacyAccuracyCounter(),
new LegacySongProgress(), new LegacySongProgress(),
new LegacyHealthDisplay(),
new BarHitErrorMeter(), new BarHitErrorMeter(),
} }
}; };

View File

@ -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,8 +289,7 @@ 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 =>
@ -307,6 +311,62 @@ 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.Name == nameof(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)
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,7 @@ namespace osu.Game.Skinning
public bool Protected { get; set; } public bool Protected { get; set; }
public virtual Skin CreateInstance(IStorageResourceProvider resources) public 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,12 +52,14 @@ 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 virtual Skin CreateInstance(IStorageResourceProvider resources) => (Skin)Activator.CreateInstance(GetInstanceType(), this, resources)!;
public virtual IList<RealmNamedFileUsage> Files { get; } = null!; public virtual IList<RealmNamedFileUsage> Files { get; } = null!;
public bool DeletePending { get; set; } public bool DeletePending { get; set; }

View File

@ -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;