1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-19 17:00:17 +08:00
Files
osu-lazer/osu.Game/Screens/Edit/Setup/ResourcesSection.cs
T
Dean Herbert b71281bec8 Fix osu!mania beatmap objects getting corrupted when updating beatmap background
Closes https://github.com/ppy/osu/issues/32825.

Tested manually to fix the issue. Setting up test coverage for this is
going to likely take over an hour compared to the 30 second fix, so
please advise if required. I couldn't find any existing tests which
perform this flow.
2025-04-17 16:11:03 +09:00

243 lines
9.8 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.Overlays;
using osu.Game.Localisation;
using osu.Game.Models;
using osu.Game.Screens.Backgrounds;
using osu.Game.Utils;
namespace osu.Game.Screens.Edit.Setup
{
public partial class ResourcesSection : SetupSection
{
private FormBeatmapFileSelector audioTrackChooser = null!;
private FormBeatmapFileSelector backgroundChooser = null!;
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,
},
};
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).RefreshBackground());
return true;
}
public bool ChangeAudioTrack(FileInfo source, bool applyToAllDifficulties)
{
if (!source.Exists)
return false;
TagLib.File? tagSource;
try
{
tagSource = TagLib.File.Create(source.FullName);
}
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;
string artist = tagSource.Tag.JoinedAlbumArtists;
if (!string.IsNullOrWhiteSpace(artist))
{
metadata.ArtistUnicode = artist;
metadata.Artist = MetadataUtils.StripNonRomanisedCharacters(metadata.ArtistUnicode);
}
string title = tagSource.Tag.Title;
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;
}
}
}
}