2019-01-24 16:43:03 +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.
2018-04-13 17:19:50 +08:00
2022-06-17 15:37:17 +08:00
#nullable disable
2018-02-23 12:22:33 +08:00
using System ;
2018-02-15 12:45:39 +08:00
using System.Collections.Generic ;
using System.Linq ;
2018-02-23 12:22:33 +08:00
using System.Linq.Expressions ;
2019-05-28 17:59:21 +08:00
using System.Threading ;
using System.Threading.Tasks ;
2021-11-29 16:15:48 +08:00
using JetBrains.Annotations ;
2018-02-22 16:16:48 +08:00
using osu.Framework.Audio ;
2018-03-20 15:26:36 +08:00
using osu.Framework.Audio.Sample ;
2019-02-21 18:04:31 +08:00
using osu.Framework.Bindables ;
2018-03-20 15:26:36 +08:00
using osu.Framework.Graphics ;
2022-08-02 18:50:57 +08:00
using osu.Framework.Graphics.Rendering ;
2018-03-20 15:28:39 +08:00
using osu.Framework.Graphics.Textures ;
2019-08-29 15:38:39 +08:00
using osu.Framework.IO.Stores ;
2018-02-15 12:45:39 +08:00
using osu.Framework.Platform ;
2021-11-29 17:07:32 +08:00
using osu.Framework.Threading ;
2020-11-11 12:05:03 +08:00
using osu.Framework.Utils ;
2019-08-23 19:32:43 +08:00
using osu.Game.Audio ;
2018-02-15 12:45:39 +08:00
using osu.Game.Database ;
2020-12-21 14:14:32 +08:00
using osu.Game.IO ;
2021-11-25 14:14:43 +08:00
using osu.Game.Overlays.Notifications ;
2022-04-01 13:11:53 +08:00
using osu.Game.Utils ;
2018-04-13 17:19:50 +08:00
2018-02-15 12:45:39 +08:00
namespace osu.Game.Skinning
{
2021-06-22 08:06:39 +08:00
/// <summary>
/// Handles the storage and retrieval of <see cref="Skin"/>s.
/// </summary>
/// <remarks>
2021-06-22 17:05:17 +08:00
/// This is also exposed and cached as <see cref="ISkinSource"/> to allow for any component to potentially have skinning support.
/// For gameplay components, see <see cref="RulesetSkinProvidingContainer"/> which adds extra legacy and toggle logic that may affect the lookup process.
2021-06-22 08:06:39 +08:00
/// </remarks>
2022-06-16 17:53:13 +08:00
public class SkinManager : ModelManager < SkinInfo > , ISkinSource , IStorageResourceProvider , IModelImporter < SkinInfo >
2018-02-15 12:45:39 +08:00
{
2022-09-19 21:46:55 +08:00
/// <summary>
/// The default "classic" skin.
/// </summary>
public Skin DefaultClassicSkin { get ; }
2018-02-22 16:16:48 +08:00
private readonly AudioManager audio ;
2018-04-13 17:19:50 +08:00
2021-11-29 15:18:34 +08:00
private readonly Scheduler scheduler ;
2020-12-21 14:14:32 +08:00
private readonly GameHost host ;
2021-05-31 17:37:32 +08:00
private readonly IResourceStore < byte [ ] > resources ;
2019-08-29 15:38:39 +08:00
2021-05-27 10:48:48 +08:00
public readonly Bindable < Skin > CurrentSkin = new Bindable < Skin > ( ) ;
2021-11-29 16:15:26 +08:00
2022-09-15 15:02:57 +08:00
public readonly Bindable < Live < SkinInfo > > CurrentSkinInfo = new Bindable < Live < SkinInfo > > ( ArgonSkin . CreateInfo ( ) . ToLiveUnmanaged ( ) ) ;
2018-04-13 17:19:50 +08:00
2022-06-16 17:11:50 +08:00
private readonly SkinImporter skinImporter ;
2018-11-28 18:16:05 +08:00
2022-12-11 17:30:24 +08:00
private readonly LegacySkinExporter skinExporter ;
2021-11-25 14:14:43 +08:00
private readonly IResourceStore < byte [ ] > userFiles ;
2018-08-31 17:28:53 +08:00
2022-09-15 15:02:57 +08:00
private Skin argonSkin { get ; }
private Skin trianglesSkin { get ; }
2021-06-07 23:42:50 +08:00
2023-01-10 00:10:20 +08:00
public override bool PauseImports
{
get = > base . PauseImports ;
set
{
base . PauseImports = value ;
skinImporter . PauseImports = value ;
}
}
2022-01-24 18:59:58 +08:00
public SkinManager ( Storage storage , RealmAccess realm , GameHost host , IResourceStore < byte [ ] > resources , AudioManager audio , Scheduler scheduler )
2022-06-16 17:53:13 +08:00
: base ( storage , realm )
2018-11-28 18:01:22 +08:00
{
this . audio = audio ;
2021-11-29 15:18:34 +08:00
this . scheduler = scheduler ;
2020-12-21 14:14:32 +08:00
this . host = host ;
2021-05-31 17:37:32 +08:00
this . resources = resources ;
2018-11-28 18:01:22 +08:00
2021-11-29 17:07:32 +08:00
userFiles = new StorageBackedResourceStore ( storage . GetStorageForDirectory ( "files" ) ) ;
2021-11-25 14:14:43 +08:00
2022-06-16 18:48:18 +08:00
skinImporter = new SkinImporter ( storage , realm , this )
{
PostNotification = obj = > PostNotification ? . Invoke ( obj ) ,
} ;
2021-11-25 14:14:43 +08:00
2021-11-29 16:15:26 +08:00
var defaultSkins = new [ ]
{
2022-09-18 17:18:10 +08:00
DefaultClassicSkin = new DefaultLegacySkin ( this ) ,
2022-09-15 15:02:57 +08:00
trianglesSkin = new TrianglesSkin ( this ) ,
argonSkin = new ArgonSkin ( this ) ,
2022-12-12 14:52:29 +08:00
new ArgonProSkin ( this ) ,
2021-11-29 16:15:26 +08:00
} ;
// Ensure the default entries are present.
2022-01-25 12:09:47 +08:00
realm . Write ( r = >
2021-11-29 16:15:26 +08:00
{
foreach ( var skin in defaultSkins )
{
2022-01-25 12:09:47 +08:00
if ( r . Find < SkinInfo > ( skin . SkinInfo . ID ) = = null )
r . Add ( skin . SkinInfo . Value ) ;
2021-11-29 16:15:26 +08:00
}
2022-01-21 16:08:20 +08:00
} ) ;
2021-06-07 23:42:50 +08:00
2022-03-24 18:09:17 +08:00
CurrentSkinInfo . ValueChanged + = skin = >
{
CurrentSkin . Value = skin . NewValue . PerformRead ( GetSkin ) ;
} ;
2021-05-27 13:50:42 +08:00
2022-09-15 15:02:57 +08:00
CurrentSkin . Value = argonSkin ;
2018-11-28 18:01:22 +08:00
CurrentSkin . ValueChanged + = skin = >
{
2021-11-29 17:07:32 +08:00
if ( ! skin . NewValue . SkinInfo . Equals ( CurrentSkinInfo . Value ) )
2018-11-28 18:01:22 +08:00
throw new InvalidOperationException ( $"Setting {nameof(CurrentSkin)}'s value directly is not supported. Use {nameof(CurrentSkinInfo)} instead." ) ;
SourceChanged ? . Invoke ( ) ;
} ;
2022-12-11 17:30:24 +08:00
2023-04-09 21:15:00 +08:00
skinExporter = new LegacySkinExporter ( storage )
2022-12-11 17:30:24 +08:00
{
PostNotification = obj = > PostNotification ? . Invoke ( obj )
} ;
2018-02-15 12:45:39 +08:00
}
2018-04-13 17:19:50 +08:00
2020-11-11 12:05:03 +08:00
public void SelectRandomSkin ( )
{
2022-06-16 17:56:53 +08:00
Realm . Run ( r = >
2020-11-11 12:05:03 +08:00
{
2021-11-29 17:07:32 +08:00
// choose from only user skins, removing the current selection to ensure a new one is chosen.
2022-09-12 18:51:49 +08:00
var randomChoices = r . All < SkinInfo > ( )
. Where ( s = > ! s . DeletePending & & s . ID ! = CurrentSkinInfo . Value . ID )
. ToArray ( ) ;
2020-11-11 12:05:03 +08:00
2021-11-29 17:07:32 +08:00
if ( randomChoices . Length = = 0 )
{
2022-09-15 15:02:57 +08:00
CurrentSkinInfo . Value = ArgonSkin . CreateInfo ( ) . ToLiveUnmanaged ( ) ;
2021-11-29 17:07:32 +08:00
return ;
}
var chosen = randomChoices . ElementAt ( RNG . Next ( 0 , randomChoices . Length ) ) ;
2022-06-16 17:56:53 +08:00
CurrentSkinInfo . Value = chosen . ToLive ( Realm ) ;
2022-01-21 00:34:20 +08:00
} ) ;
2018-03-14 18:35:43 +08:00
}
2018-04-13 17:19:50 +08:00
2018-02-22 16:16:48 +08:00
/// <summary>
/// Retrieve a <see cref="Skin"/> instance for the provided <see cref="SkinInfo"/>
/// </summary>
/// <param name="skinInfo">The skin to lookup.</param>
/// <returns>A <see cref="Skin"/> instance correlating to the provided <see cref="SkinInfo"/>.</returns>
2021-05-31 17:58:40 +08:00
public Skin GetSkin ( SkinInfo skinInfo ) = > skinInfo . CreateInstance ( this ) ;
2021-05-11 16:00:56 +08:00
/// <summary>
/// Ensure that the current skin is in a state it can accept user modifications.
/// This will create a copy of any internal skin and being tracking in the database if not already.
/// </summary>
2022-04-28 13:09:30 +08:00
/// <returns>
/// Whether a new skin was created to allow for mutation.
/// </returns>
public bool EnsureMutableSkin ( )
2018-02-22 16:16:48 +08:00
{
2022-04-28 13:09:30 +08:00
return CurrentSkinInfo . Value . PerformRead ( s = >
2021-05-11 16:00:56 +08:00
{
2021-12-02 12:41:20 +08:00
if ( ! s . Protected )
2022-04-28 13:09:30 +08:00
return false ;
2021-11-29 17:07:32 +08:00
2022-06-16 17:56:53 +08:00
string [ ] existingSkinNames = Realm . Run ( r = > r . All < SkinInfo > ( )
2022-04-01 15:19:00 +08:00
. Where ( skin = > ! skin . DeletePending )
. AsEnumerable ( )
2022-04-01 18:30:16 +08:00
. Select ( skin = > skin . Name ) . ToArray ( ) ) ;
2022-04-01 13:11:53 +08:00
2021-11-29 17:07:32 +08:00
// if the user is attempting to save one of the default skin implementations, create a copy first.
2022-04-01 13:11:53 +08:00
var skinInfo = new SkinInfo
2021-11-29 17:07:32 +08:00
{
Creator = s . Creator ,
InstantiationInfo = s . InstantiationInfo ,
2022-04-28 13:09:30 +08:00
Name = NamingUtils . GetNextBestName ( existingSkinNames , $@"{s.Name} (modified)" )
2022-04-01 13:11:53 +08:00
} ;
2023-10-27 20:34:30 +08:00
var result = skinImporter . ImportModel ( skinInfo , parameters : new ImportParameters
{
ImportImmediately = true // to avoid possible deadlocks when editing skin during gameplay.
} ) ;
2021-11-29 17:07:32 +08:00
if ( result ! = null )
2021-12-02 16:42:16 +08:00
{
// save once to ensure the required json content is populated.
// currently this only happens on save.
result . PerformRead ( skin = > Save ( skin . CreateInstance ( this ) ) ) ;
2021-11-29 17:07:32 +08:00
CurrentSkinInfo . Value = result ;
2022-04-29 12:46:18 +08:00
return true ;
2021-12-02 16:42:16 +08:00
}
2022-04-28 13:09:30 +08:00
2022-04-29 12:46:18 +08:00
return false ;
2021-11-29 17:07:32 +08:00
} ) ;
2018-02-22 16:16:48 +08:00
}
2018-04-13 17:19:50 +08:00
2023-02-02 17:42:33 +08:00
/// <summary>
2023-02-03 14:18:01 +08:00
/// Save a skin, serialising any changes to skin layouts to relevant JSON structures.
2023-02-02 17:42:33 +08:00
/// </summary>
/// <returns>Whether any change actually occurred.</returns>
public bool Save ( Skin skin )
2021-05-10 21:43:48 +08:00
{
2021-11-29 17:07:32 +08:00
if ( ! skin . SkinInfo . IsManaged )
2021-05-11 16:00:56 +08:00
throw new InvalidOperationException ( $"Attempting to save a skin which is not yet tracked. Call {nameof(EnsureMutableSkin)} first." ) ;
2021-05-10 21:43:48 +08:00
2023-02-02 17:42:33 +08:00
return skinImporter . Save ( skin ) ;
2021-05-10 21:43:48 +08:00
}
2018-02-23 12:22:33 +08:00
/// <summary>
/// Perform a lookup query on available <see cref="SkinInfo"/>s.
/// </summary>
/// <param name="query">The query.</param>
/// <returns>The first result for the provided query, or null if no results were found.</returns>
2022-01-26 12:37:33 +08:00
public Live < SkinInfo > Query ( Expression < Func < SkinInfo , bool > > query )
2021-11-29 17:07:32 +08:00
{
2022-06-16 17:56:53 +08:00
return Realm . Run ( r = > r . All < SkinInfo > ( ) . FirstOrDefault ( query ) ? . ToLive ( Realm ) ) ;
2021-11-29 17:07:32 +08:00
}
2018-04-13 17:19:50 +08:00
2018-03-20 15:26:36 +08:00
public event Action SourceChanged ;
2018-04-13 17:19:50 +08:00
2022-11-09 15:04:56 +08:00
public Drawable GetDrawableComponent ( ISkinComponentLookup lookup ) = > lookupWithFallback ( s = > s . GetDrawableComponent ( lookup ) ) ;
2018-04-13 17:19:50 +08:00
2021-06-06 11:17:55 +08:00
public Texture GetTexture ( string componentName , WrapMode wrapModeS , WrapMode wrapModeT ) = > lookupWithFallback ( s = > s . GetTexture ( componentName , wrapModeS , wrapModeT ) ) ;
2018-04-13 17:19:50 +08:00
2021-06-06 11:17:55 +08:00
public ISample GetSample ( ISampleInfo sampleInfo ) = > lookupWithFallback ( s = > s . GetSample ( sampleInfo ) ) ;
2018-04-13 17:19:50 +08:00
2021-06-06 11:17:55 +08:00
public IBindable < TValue > GetConfig < TLookup , TValue > ( TLookup lookup ) = > lookupWithFallback ( s = > s . GetConfig < TLookup , TValue > ( lookup ) ) ;
2020-12-21 14:14:32 +08:00
2021-06-07 23:42:50 +08:00
public ISkin FindProvider ( Func < ISkin , bool > lookupFunction )
{
2021-06-22 15:49:21 +08:00
foreach ( var source in AllSources )
{
if ( lookupFunction ( source ) )
return source ;
}
2021-06-09 17:51:34 +08:00
2021-06-07 23:42:50 +08:00
return null ;
}
2021-06-06 11:17:55 +08:00
2021-06-22 15:19:55 +08:00
public IEnumerable < ISkin > AllSources
{
get
{
yield return CurrentSkin . Value ;
2022-09-15 15:02:57 +08:00
// Skin manager provides default fallbacks.
// This handles cases where a user skin doesn't have the required resources for complete display of
// certain elements.
2022-09-18 17:18:10 +08:00
if ( CurrentSkin . Value is LegacySkin & & CurrentSkin . Value ! = DefaultClassicSkin )
yield return DefaultClassicSkin ;
2021-06-22 15:19:55 +08:00
2022-09-15 15:02:57 +08:00
if ( CurrentSkin . Value ! = trianglesSkin )
yield return trianglesSkin ;
2021-06-22 15:19:55 +08:00
}
}
2021-06-09 17:51:34 +08:00
private T lookupWithFallback < T > ( Func < ISkin , T > lookupFunction )
2021-06-06 11:17:55 +08:00
where T : class
{
2023-09-06 16:37:17 +08:00
try
2021-06-22 15:19:55 +08:00
{
2023-09-06 16:37:17 +08:00
Skin . LogLookupDebug ( this , lookupFunction , Skin . LookupDebugType . Enter ) ;
2021-06-06 11:17:55 +08:00
2023-09-06 16:37:17 +08:00
foreach ( var source in AllSources )
{
if ( lookupFunction ( source ) is T skinSourced )
return skinSourced ;
}
return null ;
}
finally
{
Skin . LogLookupDebug ( this , lookupFunction , Skin . LookupDebugType . Exit ) ;
}
2021-06-06 11:17:55 +08:00
}
2021-05-31 16:25:21 +08:00
2020-12-22 11:01:09 +08:00
#region IResourceStorageProvider
2022-08-02 18:50:57 +08:00
IRenderer IStorageResourceProvider . Renderer = > host . Renderer ;
2020-12-21 14:14:32 +08:00
AudioManager IStorageResourceProvider . AudioManager = > audio ;
2021-05-31 17:37:32 +08:00
IResourceStore < byte [ ] > IStorageResourceProvider . Resources = > resources ;
2021-11-25 14:14:43 +08:00
IResourceStore < byte [ ] > IStorageResourceProvider . Files = > userFiles ;
2022-06-16 17:56:53 +08:00
RealmAccess IStorageResourceProvider . RealmAccess = > Realm ;
2020-12-21 14:14:32 +08:00
IResourceStore < TextureUpload > IStorageResourceProvider . CreateTextureLoaderStore ( IResourceStore < byte [ ] > underlyingStore ) = > host . CreateTextureLoaderStore ( underlyingStore ) ;
2020-12-22 11:01:09 +08:00
#endregion
2021-11-25 14:14:43 +08:00
#region Implementation of IModelImporter < SkinInfo >
2022-06-20 17:21:37 +08:00
public Action < IEnumerable < Live < SkinInfo > > > PresentImport
2021-11-25 14:14:43 +08:00
{
2022-06-20 17:21:37 +08:00
set = > skinImporter . PresentImport = value ;
2021-11-25 14:14:43 +08:00
}
2022-06-16 17:11:50 +08:00
public Task Import ( params string [ ] paths ) = > skinImporter . Import ( paths ) ;
2021-11-25 14:14:43 +08:00
2022-12-13 20:03:25 +08:00
public Task Import ( ImportTask [ ] imports , ImportParameters parameters = default ) = > skinImporter . Import ( imports , parameters ) ;
2021-11-25 14:14:43 +08:00
2022-06-16 17:11:50 +08:00
public IEnumerable < string > HandledExtensions = > skinImporter . HandledExtensions ;
2021-11-25 14:14:43 +08:00
2022-12-13 20:03:25 +08:00
public Task < IEnumerable < Live < SkinInfo > > > Import ( ProgressNotification notification , ImportTask [ ] tasks , ImportParameters parameters = default ) = >
skinImporter . Import ( notification , tasks , parameters ) ;
2021-11-25 14:14:43 +08:00
2022-12-12 22:56:11 +08:00
public Task < Live < SkinInfo > > ImportAsUpdate ( ProgressNotification notification , ImportTask task , SkinInfo original ) = >
skinImporter . ImportAsUpdate ( notification , task , original ) ;
2022-07-26 14:46:29 +08:00
2024-07-01 11:07:13 +08:00
public Task < ExternalEditOperation < SkinInfo > > BeginExternalEditing ( SkinInfo model ) = > skinImporter . BeginExternalEditing ( model ) ;
2022-12-12 22:56:11 +08:00
public Task < Live < SkinInfo > > Import ( ImportTask task , ImportParameters parameters = default , CancellationToken cancellationToken = default ) = >
skinImporter . Import ( task , parameters , cancellationToken ) ;
2021-11-25 14:14:43 +08:00
2023-04-09 21:15:00 +08:00
public Task ExportCurrentSkin ( ) = > ExportSkin ( CurrentSkinInfo . Value ) ;
public Task ExportSkin ( Live < SkinInfo > skin ) = > skinExporter . ExportAsync ( skin ) ;
2022-12-11 17:30:24 +08:00
2021-11-25 14:14:43 +08:00
#endregion
2021-11-29 15:18:34 +08:00
public void Delete ( [ CanBeNull ] Expression < Func < SkinInfo , bool > > filter = null , bool silent = false )
2021-11-25 14:14:43 +08:00
{
2022-06-16 17:56:53 +08:00
Realm . Run ( r = >
2021-11-29 17:07:32 +08:00
{
2022-01-25 12:04:05 +08:00
var items = r . All < SkinInfo > ( )
. Where ( s = > ! s . Protected & & ! s . DeletePending ) ;
2021-11-29 16:15:48 +08:00
if ( filter ! = null )
items = items . Where ( filter ) ;
2021-11-29 15:18:34 +08:00
// check the removed skin is not the current user choice. if it is, switch back to default.
Guid currentUserSkin = CurrentSkinInfo . Value . ID ;
if ( items . Any ( s = > s . ID = = currentUserSkin ) )
2022-09-15 15:02:57 +08:00
scheduler . Add ( ( ) = > CurrentSkinInfo . Value = ArgonSkin . CreateInfo ( ) . ToLiveUnmanaged ( ) ) ;
2021-11-29 15:18:34 +08:00
2022-06-16 17:53:13 +08:00
Delete ( items . ToList ( ) , silent ) ;
2022-01-21 16:08:20 +08:00
} ) ;
2021-11-25 14:14:43 +08:00
}
2022-09-12 18:51:49 +08:00
public void SetSkinFromConfiguration ( string guidString )
{
Live < SkinInfo > skinInfo = null ;
if ( Guid . TryParse ( guidString , out var guid ) )
skinInfo = Query ( s = > s . ID = = guid ) ;
if ( skinInfo = = null )
{
if ( guid = = SkinInfo . CLASSIC_SKIN )
2022-09-18 17:18:10 +08:00
skinInfo = DefaultClassicSkin . SkinInfo ;
2022-09-12 18:51:49 +08:00
}
2022-09-15 15:02:57 +08:00
CurrentSkinInfo . Value = skinInfo ? ? trianglesSkin . SkinInfo ;
2022-09-12 18:51:49 +08:00
}
2018-02-15 12:45:39 +08:00
}
}