Skip to content
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

Add skin mounting flow #30226

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
Open

Conversation

smallketchup82
Copy link
Contributor

@smallketchup82 smallketchup82 commented Oct 11, 2024

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.

Here's a video demonstrating the feature:

osu.mounting.v2.mp4

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.

/// </summary>
public void AddFile(TModel item, Stream contents, string filename, Realm realm)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

As for why I touched this

From b7883f1 (#30226)

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
Member

Choose a reason for hiding this comment

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

This makes no sense and offers zero safeties.

If the thing has too high overhead, we fix later. Not as part of a huge change. Remove this commit please.

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?

// 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.

skinInfoLive.PerformWrite(skinInfo =>
{
// Not sure if this deletes the files from the storage or just the database.
skinInfo.Files.Clear();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

As the comment states. Does this also flag the files for deletion? I tried to test it but couldn't conclude whether it does or doesn't. And ModelManager seems to do roughly the same in its DeleteFile method. So I'm a bit confused here.

Copy link
Member

Choose a reason for hiding this comment

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

Don't add comments for this. Ask elsewhere.

And yes, it does. Storage is managed as a separate layer based on file references.

@smallketchup82
Copy link
Contributor Author

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

/// </summary>
public void AddFile(TModel item, Stream contents, string filename, Realm realm)
Copy link
Member

Choose a reason for hiding this comment

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

This makes no sense and offers zero safeties.

If the thing has too high overhead, we fix later. Not as part of a huge change. Remove this commit please.

skinInfoLive.PerformWrite(skinInfo =>
{
// Not sure if this deletes the files from the storage or just the database.
skinInfo.Files.Clear();
Copy link
Member

Choose a reason for hiding this comment

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

Don't add comments for this. Ask elsewhere.

And yes, it does. Storage is managed as a separate layer based on file references.

@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
@@ -37,7 +37,7 @@ public partial class ExternalEditOverlay : OsuFocusedOverlayContainer
private FillFlowContainer flow = null!;

[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink);
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
Copy link
Member

Choose a reason for hiding this comment

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

It should inherit from the skin editor's colours, no?

@peppy peppy self-requested a review October 23, 2024 07:34
Comment on lines +60 to +72
skinInfoLive.PerformWrite(skinInfo =>
{
skinInfo.Files.Clear();
string[] filesInMountedDirectory = Directory.EnumerateFiles(task.Path, "*.*", SearchOption.AllDirectories).Select(f => Path.GetRelativePath(task.Path, f)).ToArray();
foreach (string file in filesInMountedDirectory)
{
using var stream = File.OpenRead(Path.Combine(task.Path, file));
modelManager.AddFile(original, stream, file);
}
});
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we need to update the skin‘s name and creator if they were modified externally?

Copy link
Member

Choose a reason for hiding this comment

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

The process should probably match the beatmap reimport one, optimally.

Maybe it can shortcut in the case that ini/json files didn't change, if the full import takes too long (although I'd like to hope it doesn't).

Copy link
Contributor Author

@smallketchup82 smallketchup82 Nov 8, 2024

Choose a reason for hiding this comment

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

I was hoping that it would update automatically, since changing the skin.ini and creating a new skin instance for the SkinInfo sounds like it should update the skin's name in realm, but it probably isn't because we're reusing the old SkinInfo which would have the old Name property.

I'll probably end up implementing it as peppy suggested. Before processing the other files, I'll hash the two skin.ini's. If there's a change, I'll investigate what changed and reflect them in realm. If nothing was changed, we're good and we can skip that entire process. Though the full import process doesn't take very long and the shortcut might not be necessary.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants