1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-30 19:50:27 +08:00
Files
osu-lazer/osu.Game/Screens/Edit/Setup/ResourcesSection.cs
T
Dean Herbert 95648a3d27 Fix editor background taking too long to load with certain storyboards (#37038)
TL;DR the clock was being set too late, causing more transforms to be
created than necessary.

This solves the issue by way of a refactor (sorry). Overall this should
simplify handling of things as more of the logic is shared with the
known good-state `BeatmapBackgroundWithStoryboard`.

I did try without a refactor (just delaying the creation until the clock
arrives) but this version made more sense because background generally
expect to do their main load in BDL to aid in smooth transitions. And we
can't get the clock by there. (although arguably we could just use a
similar method to `BeatmapBackgroundWithStoryboard` and forego using the
editor clock).

Of note, I removed the black background overdraw hack. There are edge
cases where it will lead to weird transitions, but these are far and few
between. Basically you need a storyboard which sets the flag to hide the
beatmap background and has transparency in it. This is no longer as
flagrantly bad as things used to be (which led to the inline fix) as far
as I can tell, but feel free to prove me wrong. If this is a blocker
I'll probably just add a permanent black box (which does fix this).

Closes #36875.
2026-03-23 09:26:34 +01:00

268 lines
11 KiB
C#

// 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.IO;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Framework.Logging;
using osu.Game.Beatmaps;
using osu.Game.Localisation;
using osu.Game.Models;
using osu.Game.Overlays;
using osu.Game.Screens.Backgrounds;
using osu.Game.Screens.Edit.Components;
using osu.Game.Utils;
namespace osu.Game.Screens.Edit.Setup
{
public partial class ResourcesSection : SetupSection
{
private FormBeatmapFileSelector audioTrackChooser = null!;
private FormBeatmapFileSelector backgroundChooser = null!;
private readonly Bindable<EditorBeatmapSkin.SampleSet?> currentSampleSet = new Bindable<EditorBeatmapSkin.SampleSet?>();
public override LocalisableString Title => EditorSetupStrings.ResourcesHeader;
[Resolved]
private MusicController music { get; set; } = null!;
[Resolved]
private BeatmapManager beatmaps { get; set; } = null!;
[Resolved]
private IBindable<WorkingBeatmap> working { get; set; } = null!;
[Resolved]
private Editor? editor { get; set; }
[Resolved]
private SetupScreen setupScreen { get; set; } = null!;
private SetupScreenHeaderBackground headerBackground = null!;
[BackgroundDependencyLoader]
private void load()
{
headerBackground = new SetupScreenHeaderBackground
{
RelativeSizeAxes = Axes.X,
Height = 110,
};
bool beatmapHasMultipleDifficulties = working.Value.BeatmapSetInfo.Beatmaps.Count > 1;
Children = new Drawable[]
{
backgroundChooser = new FormBeatmapFileSelector(beatmapHasMultipleDifficulties, SupportedExtensions.IMAGE_EXTENSIONS)
{
Caption = GameplaySettingsStrings.BackgroundHeader,
PlaceholderText = EditorSetupStrings.ClickToSelectBackground,
},
audioTrackChooser = new FormBeatmapFileSelector(beatmapHasMultipleDifficulties, SupportedExtensions.AUDIO_EXTENSIONS)
{
Caption = EditorSetupStrings.AudioTrack,
PlaceholderText = EditorSetupStrings.ClickToSelectTrack,
},
new FormSampleSetChooser
{
Current = { BindTarget = currentSampleSet },
},
new FormSampleSet
{
Current = { BindTarget = currentSampleSet },
SampleAddRequested = (file, targetName) =>
{
string actualFilename = string.Concat(targetName, file.Extension);
using var stream = file.OpenRead();
beatmaps.AddFile(working.Value.BeatmapSetInfo, stream, actualFilename);
return actualFilename;
},
SampleRemoveRequested = filename =>
{
var file = working.Value.BeatmapSetInfo.GetFile(filename);
if (file != null)
beatmaps.DeleteFile(working.Value.BeatmapSetInfo, file);
}
},
};
backgroundChooser.PreviewContainer.Add(headerBackground);
if (!string.IsNullOrEmpty(working.Value.Metadata.BackgroundFile))
backgroundChooser.Current.Value = new FileInfo(working.Value.Metadata.BackgroundFile);
if (!string.IsNullOrEmpty(working.Value.Metadata.AudioFile))
audioTrackChooser.Current.Value = new FileInfo(working.Value.Metadata.AudioFile);
backgroundChooser.Current.BindValueChanged(backgroundChanged);
audioTrackChooser.Current.BindValueChanged(audioTrackChanged);
}
public bool ChangeBackgroundImage(FileInfo source, bool applyToAllDifficulties)
{
if (!source.Exists)
return false;
changeResource(source, applyToAllDifficulties, @"bg",
metadata => metadata.BackgroundFile,
(metadata, name) => metadata.BackgroundFile = name);
headerBackground.UpdateBackground();
editor?.ApplyToBackground(bg => ((EditorBackgroundScreen)bg).RefreshBackgroundAsync());
return true;
}
public bool ChangeAudioTrack(FileInfo source, bool applyToAllDifficulties)
{
if (!source.Exists)
return false;
string artist;
string title;
try
{
using (var tagSource = TagLibUtils.GetTagLibFile(source.FullName))
{
artist = tagSource.Tag.JoinedAlbumArtists ?? tagSource.Tag.JoinedPerformers;
title = tagSource.Tag.Title;
}
}
catch (Exception e)
{
Logger.Error(e, "The selected audio track appears to be corrupted. Please select another one.");
return false;
}
changeResource(source, applyToAllDifficulties, @"audio",
metadata => metadata.AudioFile,
(metadata, name) =>
{
metadata.AudioFile = name;
if (!string.IsNullOrWhiteSpace(artist))
{
metadata.ArtistUnicode = artist;
metadata.Artist = MetadataUtils.StripNonRomanisedCharacters(metadata.ArtistUnicode);
}
if (!string.IsNullOrEmpty(title))
{
metadata.TitleUnicode = title;
metadata.Title = MetadataUtils.StripNonRomanisedCharacters(metadata.TitleUnicode);
}
});
music.ReloadCurrentTrack();
setupScreen.MetadataChanged?.Invoke();
return true;
}
private void changeResource(FileInfo source, bool applyToAllDifficulties, string baseFilename, Func<BeatmapMetadata, string> readFilename, Action<BeatmapMetadata, string> writeMetadata)
{
var set = working.Value.BeatmapSetInfo;
var beatmap = working.Value.BeatmapInfo;
var otherBeatmaps = set.Beatmaps.Where(b => !b.Equals(beatmap));
// First, clean up files which will no longer be used.
if (applyToAllDifficulties)
{
foreach (var b in set.Beatmaps)
{
if (set.GetFile(readFilename(b.Metadata)) is RealmNamedFileUsage otherExistingFile)
beatmaps.DeleteFile(set, otherExistingFile);
}
}
else
{
RealmNamedFileUsage? oldFile = set.GetFile(readFilename(working.Value.Metadata));
if (oldFile != null)
{
bool oldFileUsedInOtherDiff = otherBeatmaps
.Any(b => readFilename(b.Metadata) == oldFile.Filename);
if (!oldFileUsedInOtherDiff)
beatmaps.DeleteFile(set, oldFile);
}
}
// Choose a new filename that doesn't clash with any other existing files.
string newFilename = $"{baseFilename}{source.Extension}";
if (set.GetFile(newFilename) != null)
{
string[] existingFilenames = set.Files.Select(f => f.Filename).Where(f =>
f.StartsWith(baseFilename, StringComparison.OrdinalIgnoreCase) &&
f.EndsWith(source.Extension, StringComparison.OrdinalIgnoreCase)).ToArray();
newFilename = NamingUtils.GetNextBestFilename(existingFilenames, $@"{baseFilename}{source.Extension}");
}
using (var stream = source.OpenRead())
beatmaps.AddFile(set, stream, newFilename);
if (applyToAllDifficulties)
{
foreach (var b in otherBeatmaps)
{
writeMetadata(b.Metadata, newFilename);
// save the difficulty to re-encode the .osu file, updating any reference of the old filename.
//
// note that this triggers a full save flow, including triggering a difficulty calculation.
// this is not a cheap operation and should be reconsidered in the future.
var beatmapWorking = beatmaps.GetWorkingBeatmap(b);
beatmaps.Save(b, beatmapWorking.GetPlayableBeatmap(b.Ruleset), beatmapWorking.GetSkin());
}
}
writeMetadata(beatmap.Metadata, newFilename);
// editor change handler cannot be aware of any file changes or other difficulties having their metadata modified.
// for simplicity's sake, trigger a save when changing any resource to ensure the change is correctly saved.
editor?.Save();
}
// to avoid scaring users, both background & audio choosers use fake `FileInfo`s with user-friendly filenames
// when displaying an imported beatmap rather than the actual SHA-named file in storage.
// however, that means that when a background or audio file is chosen that is broken or doesn't exist on disk when switching away from the fake files,
// the rollback could enter an infinite loop, because the fake `FileInfo`s *also* don't exist on disk - at least not in the fake location they indicate.
// to circumvent this issue, just allow rollback to proceed always without actually running any of the change logic to ensure visual consistency.
// note that this means that `Change{BackgroundImage,AudioTrack}()` are required to not have made any modifications to the beatmap files
// (or at least cleaned them up properly themselves) if they return `false`.
private bool rollingBackBackgroundChange;
private bool rollingBackAudioChange;
private void backgroundChanged(ValueChangedEvent<FileInfo?> file)
{
if (rollingBackBackgroundChange)
return;
if (file.NewValue == null || !ChangeBackgroundImage(file.NewValue, backgroundChooser.ApplyToAllDifficulties.Value))
{
rollingBackBackgroundChange = true;
backgroundChooser.Current.Value = file.OldValue;
rollingBackBackgroundChange = false;
}
}
private void audioTrackChanged(ValueChangedEvent<FileInfo?> file)
{
if (rollingBackAudioChange)
return;
if (file.NewValue == null || !ChangeAudioTrack(file.NewValue, audioTrackChooser.ApplyToAllDifficulties.Value))
{
rollingBackAudioChange = true;
audioTrackChooser.Current.Value = file.OldValue;
rollingBackAudioChange = false;
}
}
}
}