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