1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-18 01:30:05 +08:00

Fix skin editor sometimes dropping anchor/origin specification on paste (#35957)

* Add failing test for copy->paste not being idempotent

* Ensure all elements on default skins use fixed anchors

`UsesFixedAnchor` defaults to false, i.e. closest anchors. Combined with
manual anchor / origin specs on some drawables, this would get default
skins into impossible states wherein a drawable would use "closest
anchor" but also explicitly specify anchor / origin that aren't closest,
which horribly fails on attempting to copy and paste.

Frankly shocked this has gone unnoticed for this long, and I regret not
vetoing this "feature" more every time I see its tentacles spread to
produce breakage of levels yet unseen.

Does this commit contain major levels of suck? For sure. Do I have any
better ideas that wouldn't consist of a multi-day rewrite or deletion of
this "feature"? No.

* Fix skin editor always applying closest anchor / origin on paste regardless of whether the component uses fixed anchor

Self-explanatory. Should close https://github.com/ppy/osu/issues/29111
along with previous commit.
This commit is contained in:
Bartłomiej Dach
2025-12-11 05:40:48 +01:00
committed by GitHub
Unverified
parent 22825f6509
commit c4f7dee82b
12 changed files with 60 additions and 1 deletions
@@ -72,6 +72,9 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
leaderboard.Origin = Anchor.CentreLeft;
leaderboard.X = 10;
}
foreach (var d in container.OfType<ISerialisableDrawable>())
d.UsesFixedAnchor = true;
})
{
Children = new Drawable[]
@@ -57,6 +57,9 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
if (spectatorList != null)
spectatorList.Position = new Vector2(36, -66);
foreach (var d in container.OfType<ISerialisableDrawable>())
d.UsesFixedAnchor = true;
})
{
new DrawableGameplayLeaderboard(),
@@ -122,6 +122,9 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
leaderboard.Origin = Anchor.CentreLeft;
leaderboard.X = 10;
}
foreach (var d in container.OfType<ISerialisableDrawable>())
d.UsesFixedAnchor = true;
})
{
new LegacyManiaComboCounter(),
@@ -103,6 +103,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
leaderboard.Origin = Anchor.BottomLeft;
leaderboard.Position = pos;
}
foreach (var d in container.OfType<ISerialisableDrawable>())
d.UsesFixedAnchor = true;
})
{
Children = new Drawable[]
@@ -51,6 +51,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
spectatorList.Anchor = Anchor.BottomLeft;
spectatorList.Origin = Anchor.TopLeft;
}
foreach (var d in container.OfType<ISerialisableDrawable>())
d.UsesFixedAnchor = true;
})
{
RelativeSizeAxes = Axes.Both,
@@ -51,6 +51,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default
spectatorList.Origin = Anchor.TopLeft;
spectatorList.Position = new Vector2(320, -280);
}
foreach (var d in container.OfType<ISerialisableDrawable>())
d.UsesFixedAnchor = true;
})
{
RelativeSizeAxes = Axes.Both,
@@ -79,6 +79,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
spectatorList.Origin = Anchor.TopLeft;
spectatorList.Position = pos;
}
foreach (var d in container.OfType<ISerialisableDrawable>())
d.UsesFixedAnchor = true;
})
{
new LegacyDefaultComboCounter(),
@@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Newtonsoft.Json;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions;
@@ -378,6 +379,23 @@ namespace osu.Game.Tests.Visual.Gameplay
() => Is.EqualTo(3));
}
[Test]
public void TestCopyPasteIdempotency()
{
string state = null!;
AddStep("select everything", () => InputManager.Keys(PlatformAction.SelectAll));
AddStep("dump state", () =>
{
state = JsonConvert.SerializeObject(skinEditor.SelectedComponents.Cast<Drawable>().Select(s => s.CreateSerialisedInfo()).ToArray());
});
AddStep("copy", () => InputManager.Keys(PlatformAction.Copy));
AddStep("delete", () => InputManager.Keys(PlatformAction.Delete));
AddStep("paste", () => InputManager.Keys(PlatformAction.Paste));
AddAssert("pasted state equals dumped",
() => JsonConvert.SerializeObject(skinEditor.SelectedComponents.Cast<Drawable>().Select(s => s.CreateSerialisedInfo()).ToArray()),
() => Is.EqualTo(state));
}
private SkinnableContainer globalHUDTarget => Player.ChildrenOfType<SkinnableContainer>()
.Single(c => c.Lookup.Lookup == GlobalSkinnableContainers.MainHUDComponents && c.Lookup.Ruleset == null);
+3 -1
View File
@@ -529,7 +529,9 @@ namespace osu.Game.Overlays.SkinEditor
}
SelectedComponents.Add(component);
SkinSelectionHandler.ApplyClosestAnchorOrigin(drawableComponent);
if (!component.UsesFixedAnchor)
SkinSelectionHandler.ApplyClosestAnchorOrigin(drawableComponent);
return true;
}
+6
View File
@@ -128,6 +128,9 @@ namespace osu.Game.Skinning
if (spectatorList != null)
spectatorList.Position = pos;
foreach (var d in container.OfType<ISerialisableDrawable>())
d.UsesFixedAnchor = true;
})
{
RelativeSizeAxes = Axes.Both,
@@ -238,6 +241,9 @@ namespace osu.Game.Skinning
keyCounter.Position = new Vector2(-(hitError.Width + padding), -(padding * 2 + song_progress_offset_height));
}
}
foreach (var d in container.OfType<ISerialisableDrawable>())
d.UsesFixedAnchor = true;
}
})
{
+6
View File
@@ -412,6 +412,9 @@ namespace osu.Game.Skinning
leaderboard.Origin = Anchor.CentreLeft;
leaderboard.X = 10;
}
foreach (var d in container.OfType<ISerialisableDrawable>())
d.UsesFixedAnchor = true;
})
{
new LegacyDefaultComboCounter(),
@@ -448,6 +451,9 @@ namespace osu.Game.Skinning
hitError.Origin = Anchor.CentreLeft;
hitError.Rotation = -90;
}
foreach (var d in container.OfType<ISerialisableDrawable>())
d.UsesFixedAnchor = true;
})
{
Children = new Drawable[]
+6
View File
@@ -106,6 +106,9 @@ namespace osu.Game.Skinning
spectatorList.Origin = Anchor.BottomLeft;
spectatorList.Position = new Vector2(screen_edge_padding, -(song_progress_offset_height + screen_edge_padding));
}
foreach (var d in container.OfType<ISerialisableDrawable>())
d.UsesFixedAnchor = true;
})
{
RelativeSizeAxes = Axes.Both,
@@ -178,6 +181,9 @@ namespace osu.Game.Skinning
keyCounter.Origin = Anchor.BottomRight;
keyCounter.Position = new Vector2(-screen_edge_padding, -(song_progress_offset_height + screen_edge_padding));
}
foreach (var d in container.OfType<ISerialisableDrawable>())
d.UsesFixedAnchor = true;
})
{
Children = new Drawable[]