Skip to content

Add skin mounting flow #30226

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 24 commits into from
Jul 1, 2025
Merged

Add skin mounting flow #30226

merged 24 commits into from
Jul 1, 2025

Conversation

smallketchup82
Copy link
Contributor

@smallketchup82 smallketchup82 commented Oct 11, 2024

osu.mounting.v2.mp4

Similarly to the analogous option in beatmap editor, this allows arbitrary external modifications to skins. In particular this means that it is now possible to change gameplay assets without exporting and re-emporting the skin.


This pull request expands on the mounting logic from beatmaps and integrates it with the skin editor, thereby introducing external editing functionality for skins.

I have applied my current knowledge of realm and threading to ensure the code is at the very least presentable. However, I acknowledge that further refinement and evaluation may be necessary. I am open to any feedback and suggestions to improve this implementation.

This is intended to be a stopgap solution until Skin Editor gains support for editing skin files visually.

The GetFile method in AddFile has a huge overhead, given we're doing
this in a loop.

Since we clear the files in the skin, we already know there won't be any
existing files, so we can skip all of that logic
Copy link
Contributor Author

@smallketchup82 smallketchup82 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Going to leave some comments here for reviewers on why I did certain things and any concerns I have. Apologies if I shouldn't be doing it this way, please correct me for future reference if so.

foreach (var realmFile in model.Files)
// Detach files from the model to avoid realm contention when copying to the external location.
// This is safe as we are not modifying the model in any way.
foreach (var realmFile in model.Files.Detach())
Copy link
Contributor Author

@smallketchup82 smallketchup82 Oct 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I ran into an issue where detaching the skin doesn't actually end up detaching the files, resulting in a Realms.Exceptions.RealmException: Realm accessed from incorrect thread. exception.

I couldn't find a way to fix it apart from this. Fortunately, we only access the realm files here to mount them, so we aren't modifying the files in a way that requires them to be attached.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two questions:

  • Where / when is the "skin" being "detached"?
  • Does detach even... work for SkinInfo? It sure isn't registered in the automapper detach machinery.

c.CreateMap<RealmKeyBinding, RealmKeyBinding>();
c.CreateMap<BeatmapMetadata, BeatmapMetadata>();
c.CreateMap<BeatmapUserSettings, BeatmapUserSettings>();
c.CreateMap<BeatmapDifficulty, BeatmapDifficulty>();
c.CreateMap<RulesetInfo, RulesetInfo>();
c.CreateMap<ScoreInfo, ScoreInfo>();
c.CreateMap<RealmUser, RealmUser>();
c.CreateMap<RealmFile, RealmFile>();
c.CreateMap<RealmNamedFileUsage, RealmNamedFileUsage>();

Maybe adding SkinInfo in there just fixes this. I dunno.

Copy link
Contributor Author

@smallketchup82 smallketchup82 Oct 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The skin is being detached here

CurrentSkin.Value.SkinInfo in SkinEditor returns a Live<SkinInfo>. My original idea for fixing the issue with "realm being accessed from an incorrect thread" was to detach the Live<SkinInfo> so that we're only working with a SkinInfo. Though, simply stopping here (without detaching Files) doesn't work and results in the thread error. Adding in the .Detach() to model.Files fixes it.

I got here via trial and error so I don't know for certain. But my best guess is that it likely has to do with RealmNamedFileUsage being in the automapper detach machinery in which you specified. Maybe it's only detaching the Files that ends up fixing the issue, not necessarily the detaching of the Live<SkinInfo>.

At any rate, I guess detaching the Live<SkinInfo> serves only to convert from Live<SkinInfo> to SkinInfo. But a quick test without detaching the Live<SkinInfo> seems to work fine. So maybe that's unnecessary?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

self-resolved via 2e0e7ff


// Create a new skin instance to ensure the skin is reloaded
// If there's a better way to reload the skin, this should be replaced with it.
currentSkin.Value = newSkinInfo.CreateInstance(skins);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is where most of my confidence in the code quality of this PR declines. This was the only way I found that would refresh the skin instantly. I looked through at least 15 different classes, and even tests, to figure out a proper way of reloading skins. Was left empty handed and came up with this which works. The reason I feel unconfident about it is because it feels rather hacky.

@smallketchup82
Copy link
Contributor Author

Forgot tests, sorry about that. Will add them in ASAP

@peppy
Copy link
Member

peppy commented Oct 11, 2024

Forgot tests, sorry about that. Will add them in ASAP

Seems back to front. Like you should be creating the tests to develop the feature. Adding as an afterthought is mostly pointless for something like this? But let's see what you come up with.

@peppy
Copy link
Member

peppy commented Oct 11, 2024

I don't like this "simple" approach. Maybe simple for you, but not for a user. And also means you could potentially exit the skin editor or make edits in it while mounted, then overwrite those edits.

There should be a big button for the user to click once done external editing.

@smallketchup82
Copy link
Contributor Author

smallketchup82 commented Oct 11, 2024

I can look into making an overlay equivalent of ExternalEditScreen, though this might take me at least a week given my current schedule.

@smallketchup82
Copy link
Contributor Author

Sorry, misclick

@smallketchup82
Copy link
Contributor Author

I've managed to get an overlay component working. It's more or less a port of ExternalEditScreen to an overlay. With a bit of work it can be made compatible with beatmaps, and ExternalEditScreen can be replaced with it, so that the beatmap editor uses the overlay as well.

Here's a video demonstrating what the flow looks like now:

osu.mounting.v2.mp4

Figured this made sense since we're using purple buttons. It doesn't
really seem to change anything visually though
@smallketchup82 smallketchup82 changed the title Add basic skin mounting flow Add skin mounting flow Oct 13, 2024
@peppy peppy self-requested a review October 23, 2024 07:34
Peppy spoke about using a shortcut and/or hashes to determine if the
skin.ini is changed, and if so, then to rename the skin.

In my opinion, hashing and doing numerous comparisons is probably less
efficient than just syncing the SkinInfo's name during the update.

This is an easy solution that does what it needs to.
@smallketchup82
Copy link
Contributor Author

Since the last review has been addressed, this should be good to go for another round.

@bdach
Copy link
Collaborator

bdach commented Dec 12, 2024

@smallketchup82 resolved conflicts for you, please double check when able

@smallketchup82
Copy link
Contributor Author

@bdach Thanks! I didn't notice the merge conflicts. Your resolution looks good to me.

@bdach bdach self-requested a review December 16, 2024 03:02
Copy link
Collaborator

@bdach bdach left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The settings menu can be opened when the skin is externally mounted. Meaning you can change skin while the skin is externally mounted. Doing this results in fundamentally broken behaviour and it should not even be permissible to enter such a state.

Also disallows using the random skin keybind when the external edit
overlay is open. SkinEditor should already be disabling it, but I
figured we might as well add this in for redundancy
@smallketchup82
Copy link
Contributor Author

Thanks for catching that! I've fixed it in 7a5e613. Not sure if that's how or where you wanted it to be done, but it works well and disallows opening the settings menu when the external edit overlay is visible.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I heavily dislike basically the entirety of 7a5e613 and I think the root cause of all that is this class. Why is this an overlay at all? Why is it not a blocking screen like the editor implementation of this is?

Making the external skin edit thing a blocking screen should fix most of the problems that commit is trying to special-case. With the exception of the random skin hotkey, probably - that one I would argue needs better high-level handling, via the current skin bindable getting disabled or something. And even the commit message says the random thing handling may not be necessary at all...?

Copy link
Contributor Author

@smallketchup82 smallketchup82 Jan 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I created this as an overlay because I initially designed it to work around the skin editor. To me, an overlay on top of another overlay seems simpler than hiding the skin editor overlay, pushing the external edit screen, and then showing the skin editor overlay once that’s done, or any other method that involves screen usage. This was a choice I made for conceptual simplicity. My entire idea was to add the overlay version of the external edit screen for the skin editor, and then move the beatmap editor to use the overlay version in a future PR.

To be blunt, if your only issue is 7a5e613, I can revert that. It was an addition to future-proof the external edit overlay (for example, if you mount a skin outside the context of the skin editor). However, as the commit description states, it’s optional and redundant since the skin editor already performs the same task.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be blunt, if your only issue is 7a5e613, I can revert that

Uh... how does that work? That commit's fixing #30226 (review) is it not? At least the weird explicit "suppress settings being opened if the external edit overlay is visible" logic would need to remain, would it not?

Basically the fact that OsuGame has to get involved in interactions between these two extremely specific pieces of the game is making the commit really hard to accept for me.

To me, an overlay on top of another overlay seems simpler than hiding the skin editor overlay, pushing the external edit screen, and then showing the skin editor overlay once that’s done, or any other method that involves screen usage

Did you actually try it?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea of pushing a screen (if we're assuming the screen will be pushed to the game's main stack) likely isn't going to work well with gameplay, which is one of the common cases for the skin editor.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well then I don't know what the solution is, but I stand rather unmoved in opposition to OsuGame having to coordinate these two specific pieces deep inside the game. It feels not good. OsuGame is the last resort for stuff like that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't looked into the PR code yet, but can agree that it definitely should not live in OsuGame.

Copy link
Contributor Author

@smallketchup82 smallketchup82 Jan 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the delay in response, I've been busy studying for exams.

At least the weird explicit "suppress settings being opened if the external edit overlay is visible" logic would need to remain, would it not?

Yes, that's my bad. You're right here.

Did you actually try it?

On a local branch yes. It suffers the issue that peppy mentioned (opening skin editor then pressing external edit mid gameplay takes you outside of gameplay). This happens even when paused which takes a good amount of usability away from this feature. I can confirm that it otherwise works.

but I stand rather unmoved in opposition to OsuGame having to coordinate these two specific pieces deep inside the game. It feels not good

I have no issue with moving it out of OsuGame, the question I have is where? Should I move this settings panel side? Or overlay side (i.e. DI settings panel to ExternalEditOverlay and disable it within ExternalEditOverlay)? A bit of direction here would be useful.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

self-resolved via b82bf22

@bdach
Copy link
Collaborator

bdach commented Jun 30, 2025

I've done a pass on this to hopefully bring it over the finish line but disclaimer that I am letting tests run on CI and have not attempted to run tests locally. Testing was mostly manual.

If nothing funny falls out of CI I'll request a second pass over the code because I changed much.

bdach
bdach previously approved these changes Jun 30, 2025
/// Write changes to realm asynchronously, guaranteeing order of execution.
/// </summary>
/// <param name="action">The work to run.</param>
public Task<T> WriteAsync<T>(Func<Realm, T> action)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just an overload copy-pasted from non-generic WriteAsync() and adjusted to work, to support returning data from WriteAsync() calls.

@bdach bdach requested review from peppy and removed request for peppy July 1, 2025 06:53
@@ -1227,7 +1225,7 @@ protected override void LoadComplete()
loadComponentSingleFile(beatmapSetOverlay = new BeatmapSetOverlay(), overlayContent.Add, true);
loadComponentSingleFile(wikiOverlay = new WikiOverlay(), overlayContent.Add, true);
loadComponentSingleFile(skinEditor = new SkinEditorOverlay(ScreenContainer), overlayContent.Add, true);
loadComponentSingleFile(externalEditOverlay = new ExternalEditOverlay(), overlayContent.Add, true);
loadComponentSingleFile(new ExternalEditOverlay(), overlayContent.Add, true);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A bit painful to have this at a global level like this.

diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index 9e524878dc..394917dc62 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -1225,7 +1225,6 @@ protected override void LoadComplete()
             loadComponentSingleFile(beatmapSetOverlay = new BeatmapSetOverlay(), overlayContent.Add, true);
             loadComponentSingleFile(wikiOverlay = new WikiOverlay(), overlayContent.Add, true);
             loadComponentSingleFile(skinEditor = new SkinEditorOverlay(ScreenContainer), overlayContent.Add, true);
-            loadComponentSingleFile(new ExternalEditOverlay(), overlayContent.Add, true);
 
             loadComponentSingleFile(new LoginOverlay
             {
diff --git a/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs b/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs
index 344dcc0d66..8b52c6b43c 100644
--- a/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs
+++ b/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs
@@ -49,9 +49,15 @@ public partial class SkinEditorOverlay : OverlayContainer, IKeyBindingHandler<Gl
         [Resolved]
         private IPerformFromScreenRunner? performer { get; set; }
 
+        [Resolved]
+        private IOverlayManager? overlayManager { get; set; }
+
         [Cached]
         public readonly EditorClipboard Clipboard = new EditorClipboard();
 
+        [Cached]
+        private readonly ExternalEditOverlay externalEditOverlay = new ExternalEditOverlay();
+
         [Resolved]
         private OsuGame game { get; set; } = null!;
 
@@ -86,6 +92,13 @@ private void load(OsuConfigManager config)
             config.BindWith(OsuSetting.BeatmapSkins, beatmapSkins);
         }
 
+        protected override void LoadComplete()
+        {
+            base.LoadComplete();
+
+            overlayManager?.RegisterBlockingOverlay(externalEditOverlay);
+        }
+
         public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
         {
             switch (e.Action)

could work? at least in manual testing it looks to still work.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

applied with adjustments (disposal of the Disposable that RegisterBlockingOverlay() returns) in 2c22158

Copy link
Member

@peppy peppy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fine apart from proposed change.

@bdach bdach added the notable feature Attach to pull requests which should be highlighted on the changelog. Things users want to read. label Jul 1, 2025
@peppy peppy merged commit 6470295 into ppy:master Jul 1, 2025
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area:skin-editor notable feature Attach to pull requests which should be highlighted on the changelog. Things users want to read. size/L
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants