2020-10-06 14:17:15 +08:00
// 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.
2024-11-24 13:32:50 +08:00
using System ;
2020-10-06 14:17:15 +08:00
using System.IO ;
2024-11-24 13:32:50 +08:00
using System.Linq ;
2020-10-06 14:17:15 +08:00
using osu.Framework.Allocation ;
using osu.Framework.Bindables ;
using osu.Framework.Graphics ;
2021-04-04 01:02:33 +08:00
using osu.Framework.Localisation ;
2025-01-27 17:25:22 +08:00
using osu.Framework.Logging ;
2020-10-06 14:17:15 +08:00
using osu.Game.Beatmaps ;
using osu.Game.Overlays ;
2022-08-11 01:53:20 +08:00
using osu.Game.Localisation ;
2024-11-24 13:32:50 +08:00
using osu.Game.Models ;
2024-12-31 20:57:50 +08:00
using osu.Game.Screens.Backgrounds ;
2024-11-24 13:32:50 +08:00
using osu.Game.Utils ;
2020-10-06 14:17:15 +08:00
namespace osu.Game.Screens.Edit.Setup
{
2024-10-03 19:53:21 +08:00
public partial class ResourcesSection : SetupSection
2020-10-06 14:17:15 +08:00
{
2024-11-27 18:53:22 +08:00
private FormBeatmapFileSelector audioTrackChooser = null ! ;
private FormBeatmapFileSelector backgroundChooser = null ! ;
2020-10-06 14:17:15 +08:00
2022-08-15 23:14:16 +08:00
public override LocalisableString Title = > EditorSetupStrings . ResourcesHeader ;
2021-04-04 01:02:33 +08:00
2020-10-06 14:17:15 +08:00
[Resolved]
2023-01-07 08:08:02 +08:00
private MusicController music { get ; set ; } = null ! ;
2020-10-06 14:17:15 +08:00
[Resolved]
2023-01-07 08:08:02 +08:00
private BeatmapManager beatmaps { get ; set ; } = null ! ;
2020-10-06 14:17:15 +08:00
2021-01-04 15:47:08 +08:00
[Resolved]
2023-01-07 08:08:02 +08:00
private IBindable < WorkingBeatmap > working { get ; set ; } = null ! ;
2021-01-04 15:47:08 +08:00
2023-01-07 00:26:30 +08:00
[Resolved]
2023-01-07 08:03:52 +08:00
private Editor ? editor { get ; set ; }
2023-01-07 00:26:30 +08:00
2024-12-27 22:07:24 +08:00
[Resolved]
private SetupScreen setupScreen { get ; set ; } = null ! ;
2024-10-04 17:09:14 +08:00
private SetupScreenHeaderBackground headerBackground = null ! ;
2021-04-04 18:50:50 +08:00
2020-10-06 14:17:15 +08:00
[BackgroundDependencyLoader]
private void load ( )
{
2024-10-04 17:09:14 +08:00
headerBackground = new SetupScreenHeaderBackground
{
RelativeSizeAxes = Axes . X ,
Height = 110 ,
} ;
2024-11-28 14:13:32 +08:00
bool beatmapHasMultipleDifficulties = working . Value . BeatmapSetInfo . Beatmaps . Count > 1 ;
2024-11-27 18:53:22 +08:00
2020-10-06 18:26:57 +08:00
Children = new Drawable [ ]
2020-10-06 14:17:15 +08:00
{
2024-12-04 17:31:15 +08:00
backgroundChooser = new FormBeatmapFileSelector ( beatmapHasMultipleDifficulties , SupportedExtensions . IMAGE_EXTENSIONS )
2021-04-04 18:50:50 +08:00
{
2024-08-28 18:17:39 +08:00
Caption = GameplaySettingsStrings . BackgroundHeader ,
PlaceholderText = EditorSetupStrings . ClickToSelectBackground ,
2021-04-04 18:50:50 +08:00
} ,
2024-12-04 17:31:15 +08:00
audioTrackChooser = new FormBeatmapFileSelector ( beatmapHasMultipleDifficulties , SupportedExtensions . AUDIO_EXTENSIONS )
2020-10-06 14:17:15 +08:00
{
2024-08-28 18:17:39 +08:00
Caption = EditorSetupStrings . AudioTrack ,
PlaceholderText = EditorSetupStrings . ClickToSelectTrack ,
2021-08-23 23:01:01 +08:00
} ,
2020-10-06 14:17:15 +08:00
} ;
2024-10-04 17:09:14 +08:00
backgroundChooser . PreviewContainer . Add ( headerBackground ) ;
2022-06-16 00:29:09 +08:00
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 ) ;
2022-06-16 23:48:32 +08:00
backgroundChooser . Current . BindValueChanged ( backgroundChanged ) ;
audioTrackChooser . Current . BindValueChanged ( audioTrackChanged ) ;
2020-10-06 14:17:15 +08:00
}
2024-11-27 18:53:22 +08:00
public bool ChangeBackgroundImage ( FileInfo source , bool applyToAllDifficulties )
2020-10-06 14:17:15 +08:00
{
2022-06-15 14:02:48 +08:00
if ( ! source . Exists )
2020-10-06 14:17:15 +08:00
return false ;
2024-11-29 06:57:47 +08:00
changeResource ( source , applyToAllDifficulties , @"bg" ,
metadata = > metadata . BackgroundFile ,
( metadata , name ) = > metadata . BackgroundFile = name ) ;
2022-08-01 15:36:12 +08:00
2024-08-28 18:17:39 +08:00
headerBackground . UpdateBackground ( ) ;
2024-12-31 20:57:50 +08:00
editor ? . ApplyToBackground ( bg = > ( ( EditorBackgroundScreen ) bg ) . RefreshBackground ( ) ) ;
2020-10-06 14:17:15 +08:00
return true ;
}
2024-11-27 18:53:22 +08:00
public bool ChangeAudioTrack ( FileInfo source , bool applyToAllDifficulties )
2021-04-04 18:50:50 +08:00
{
2022-06-15 14:02:48 +08:00
if ( ! source . Exists )
2021-04-04 18:50:50 +08:00
return false ;
2025-01-27 17:25:22 +08:00
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 ;
}
2024-12-27 22:07:24 +08:00
2024-11-29 06:57:47 +08:00
changeResource ( source , applyToAllDifficulties , @"audio" ,
metadata = > metadata . AudioFile ,
2024-12-27 22:07:24 +08:00
( 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 ) ;
}
} ) ;
2024-11-29 06:57:47 +08:00
music . ReloadCurrentTrack ( ) ;
2024-12-27 22:07:24 +08:00
setupScreen . MetadataChanged ? . Invoke ( ) ;
2024-11-29 06:57:47 +08:00
return true ;
}
2024-12-27 22:07:24 +08:00
private void changeResource ( FileInfo source , bool applyToAllDifficulties , string baseFilename , Func < BeatmapMetadata , string > readFilename , Action < BeatmapMetadata , string > writeMetadata )
2024-11-29 06:57:47 +08:00
{
2021-04-04 18:50:50 +08:00
var set = working . Value . BeatmapSetInfo ;
2024-12-10 15:40:47 +08:00
var beatmap = working . Value . BeatmapInfo ;
2021-04-04 18:50:50 +08:00
2024-12-10 15:40:47 +08:00
var otherBeatmaps = set . Beatmaps . Where ( b = > ! b . Equals ( beatmap ) ) ;
2024-11-29 06:57:47 +08:00
2024-12-10 15:40:47 +08:00
// First, clean up files which will no longer be used.
2024-11-27 18:53:22 +08:00
if ( applyToAllDifficulties )
2024-11-24 13:32:27 +08:00
{
2024-12-10 15:40:47 +08:00
foreach ( var b in set . Beatmaps )
2024-11-27 18:53:22 +08:00
{
2024-12-10 15:40:47 +08:00
if ( set . GetFile ( readFilename ( b . Metadata ) ) is RealmNamedFileUsage otherExistingFile )
2024-11-29 07:32:03 +08:00
beatmaps . DeleteFile ( set , otherExistingFile ) ;
2024-11-27 18:53:22 +08:00
}
}
else
{
2024-12-10 15:40:47 +08:00
RealmNamedFileUsage ? oldFile = set . GetFile ( readFilename ( working . Value . Metadata ) ) ;
2024-11-24 13:32:50 +08:00
2024-12-10 15:40:47 +08:00
if ( oldFile ! = null )
2024-11-24 13:32:50 +08:00
{
2024-12-10 15:40:47 +08:00
bool oldFileUsedInOtherDiff = otherBeatmaps
. Any ( b = > readFilename ( b . Metadata ) = = oldFile . Filename ) ;
if ( ! oldFileUsedInOtherDiff )
beatmaps . DeleteFile ( set , oldFile ) ;
2024-11-24 13:32:50 +08:00
}
2024-12-10 15:40:47 +08:00
}
2024-11-24 13:32:50 +08:00
2024-12-10 15:40:47 +08:00
// Choose a new filename that doesn't clash with any other existing files.
string newFilename = $"{baseFilename}{source.Extension}" ;
2024-11-29 06:57:47 +08:00
2024-12-10 15:40:47 +08:00
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}" ) ;
2024-11-27 18:53:22 +08:00
}
2024-11-24 13:32:50 +08:00
2024-11-27 18:53:22 +08:00
using ( var stream = source . OpenRead ( ) )
2024-11-29 06:57:47 +08:00
beatmaps . AddFile ( set , stream , newFilename ) ;
2024-11-24 13:32:50 +08:00
2024-12-10 15:40:47 +08:00
if ( applyToAllDifficulties )
{
foreach ( var b in otherBeatmaps )
{
2024-12-27 22:07:24 +08:00
writeMetadata ( b . Metadata , newFilename ) ;
2024-12-10 15:44:35 +08:00
// 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 . Beatmap , beatmapWorking . GetSkin ( ) ) ;
2024-12-10 15:40:47 +08:00
}
}
2024-12-27 22:07:24 +08:00
writeMetadata ( beatmap . Metadata , newFilename ) ;
2024-12-10 15:40:47 +08:00
2024-11-29 07:32:03 +08:00
// 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 ( ) ;
2024-11-24 13:32:50 +08:00
}
2025-01-27 17:25:53 +08:00
// 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 ;
2023-01-07 08:08:02 +08:00
private void backgroundChanged ( ValueChangedEvent < FileInfo ? > file )
2021-04-04 18:50:50 +08:00
{
2025-01-27 17:25:53 +08:00
if ( rollingBackBackgroundChange )
return ;
2024-11-27 18:53:22 +08:00
if ( file . NewValue = = null | | ! ChangeBackgroundImage ( file . NewValue , backgroundChooser . ApplyToAllDifficulties . Value ) )
2025-01-27 17:25:53 +08:00
{
rollingBackBackgroundChange = true ;
2022-06-16 23:48:32 +08:00
backgroundChooser . Current . Value = file . OldValue ;
2025-01-27 17:25:53 +08:00
rollingBackBackgroundChange = false ;
}
2021-04-04 18:50:50 +08:00
}
2023-01-07 08:08:02 +08:00
private void audioTrackChanged ( ValueChangedEvent < FileInfo ? > file )
2020-10-06 14:17:15 +08:00
{
2025-01-27 17:25:53 +08:00
if ( rollingBackAudioChange )
return ;
2024-11-27 18:53:22 +08:00
if ( file . NewValue = = null | | ! ChangeAudioTrack ( file . NewValue , audioTrackChooser . ApplyToAllDifficulties . Value ) )
2025-01-27 17:25:53 +08:00
{
rollingBackAudioChange = true ;
2022-06-16 23:48:32 +08:00
audioTrackChooser . Current . Value = file . OldValue ;
2025-01-27 17:25:53 +08:00
rollingBackAudioChange = false ;
}
2020-10-06 14:17:15 +08:00
}
}
}