2019-09-03 16:57:34 +08:00
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
2019-01-24 16:43:03 +08:00
// See the LICENCE file in the repository root for full licence text.
2018-04-13 17:19:50 +08:00
2018-03-14 19:45:04 +08:00
using System ;
2021-05-10 21:43:48 +08:00
using System.Collections.Generic ;
2022-03-23 23:08:01 +08:00
using System.Diagnostics ;
2021-10-22 13:41:59 +08:00
using System.IO ;
2021-05-10 21:43:48 +08:00
using System.Linq ;
using System.Text ;
2023-01-24 14:48:57 +08:00
using System.Threading ;
using System.Threading.Tasks ;
2021-05-10 21:43:48 +08:00
using Newtonsoft.Json ;
2018-02-22 16:16:48 +08:00
using osu.Framework.Audio.Sample ;
2019-09-03 16:57:34 +08:00
using osu.Framework.Bindables ;
2023-01-24 14:48:57 +08:00
using osu.Framework.Extensions.ObjectExtensions ;
2018-02-22 16:16:48 +08:00
using osu.Framework.Graphics ;
2018-03-20 15:28:39 +08:00
using osu.Framework.Graphics.Textures ;
2022-03-22 18:07:05 +08:00
using osu.Framework.IO.Stores ;
2021-10-01 21:15:10 +08:00
using osu.Framework.Logging ;
2019-08-23 19:32:43 +08:00
using osu.Game.Audio ;
2021-11-29 17:02:09 +08:00
using osu.Game.Database ;
2021-05-10 21:43:48 +08:00
using osu.Game.IO ;
using osu.Game.Screens.Play.HUD ;
2023-01-24 14:48:57 +08:00
using SixLabors.ImageSharp ;
using SixLabors.ImageSharp.Processing ;
2018-04-13 17:19:50 +08:00
2018-02-22 16:16:48 +08:00
namespace osu.Game.Skinning
{
2019-04-25 16:36:17 +08:00
public abstract class Skin : IDisposable , ISkin
2018-02-22 16:16:48 +08:00
{
2022-03-22 17:36:42 +08:00
/// <summary>
2022-03-24 11:39:47 +08:00
/// A texture store which can be used to perform user file lookups for this skin.
2022-03-22 17:36:42 +08:00
/// </summary>
2022-03-23 23:08:01 +08:00
protected TextureStore ? Textures { get ; }
2022-03-22 17:36:42 +08:00
/// <summary>
2022-03-24 11:39:47 +08:00
/// A sample store which can be used to perform user file lookups for this skin.
2022-03-22 17:36:42 +08:00
/// </summary>
2022-03-23 23:08:01 +08:00
protected ISampleStore ? Samples { get ; }
2022-03-22 17:36:42 +08:00
2022-01-26 12:37:33 +08:00
public readonly Live < SkinInfo > SkinInfo ;
2018-04-13 17:19:50 +08:00
2021-10-22 13:41:59 +08:00
public SkinConfiguration Configuration { get ; set ; }
2018-04-13 17:19:50 +08:00
2022-11-09 15:04:56 +08:00
public IDictionary < GlobalSkinComponentLookup . LookupType , SkinnableInfo [ ] > DrawableComponentInfo = > drawableComponentInfo ;
2021-05-10 21:43:48 +08:00
2022-11-09 15:04:56 +08:00
private readonly Dictionary < GlobalSkinComponentLookup . LookupType , SkinnableInfo [ ] > drawableComponentInfo = new Dictionary < GlobalSkinComponentLookup . LookupType , SkinnableInfo [ ] > ( ) ;
2018-04-13 17:19:50 +08:00
2022-03-23 23:08:01 +08:00
public abstract ISample ? GetSample ( ISampleInfo sampleInfo ) ;
2018-04-13 17:19:50 +08:00
2022-03-23 23:08:01 +08:00
public Texture ? GetTexture ( string componentName ) = > GetTexture ( componentName , default , default ) ;
2020-07-17 15:54:30 +08:00
2022-03-23 23:08:01 +08:00
public abstract Texture ? GetTexture ( string componentName , WrapMode wrapModeS , WrapMode wrapModeT ) ;
2018-04-13 17:19:50 +08:00
2022-03-25 14:53:55 +08:00
public abstract IBindable < TValue > ? GetConfig < TLookup , TValue > ( TLookup lookup )
where TLookup : notnull
where TValue : notnull ;
2018-04-13 17:19:50 +08:00
2022-03-25 16:31:03 +08:00
private readonly RealmBackedResourceStore < SkinInfo > ? realmBackedStorage ;
2022-03-24 18:09:17 +08:00
2022-03-23 12:14:56 +08:00
/// <summary>
/// Construct a new skin.
/// </summary>
/// <param name="skin">The skin's metadata. Usually a live realm object.</param>
/// <param name="resources">Access to game-wide resources.</param>
2022-03-24 11:45:11 +08:00
/// <param name="storage">An optional store which will *replace* all file lookups that are usually sourced from <paramref name="skin"/>.</param>
2022-03-23 13:53:00 +08:00
/// <param name="configurationFilename">An optional filename to read the skin configuration from. If not provided, the configuration will be retrieved from the storage using "skin.ini".</param>
2022-03-23 23:08:01 +08:00
protected Skin ( SkinInfo skin , IStorageResourceProvider ? resources , IResourceStore < byte [ ] > ? storage = null , string configurationFilename = @"skin.ini" )
2018-02-22 16:16:48 +08:00
{
2022-03-22 18:07:05 +08:00
if ( resources ! = null )
2022-03-22 17:36:42 +08:00
{
2022-03-23 23:08:01 +08:00
SkinInfo = skin . ToLive ( resources . RealmAccess ) ;
2022-03-23 13:21:35 +08:00
2022-03-24 18:09:17 +08:00
storage ? ? = realmBackedStorage = new RealmBackedResourceStore < SkinInfo > ( SkinInfo , resources . Files , resources . RealmAccess ) ;
2022-03-23 13:21:35 +08:00
var samples = resources . AudioManager ? . GetSampleStore ( storage ) ;
if ( samples ! = null )
samples . PlaybackConcurrency = OsuGameBase . SAMPLE_CONCURRENCY ;
2022-08-04 13:48:12 +08:00
// osu-stable performs audio lookups in order of wav -> mp3 -> ogg.
// The GetSampleStore() call above internally adds wav and mp3, so ogg is added at the end to ensure expected ordering.
( storage as ResourceStore < byte [ ] > ) ? . AddExtension ( "ogg" ) ;
2022-03-23 13:21:35 +08:00
Samples = samples ;
2023-01-24 14:48:57 +08:00
Textures = new TextureStore ( resources . Renderer , new SquishingTextureLoaderStore ( resources . CreateTextureLoaderStore ( storage ) ) ) ;
2022-03-22 17:36:42 +08:00
}
2022-03-23 23:08:01 +08:00
else
{
// Generally only used for tests.
SkinInfo = skin . ToLiveUnmanaged ( ) ;
}
2022-03-22 17:36:42 +08:00
2022-03-23 13:53:00 +08:00
var configurationStream = storage ? . GetStream ( configurationFilename ) ;
2021-10-22 13:41:59 +08:00
if ( configurationStream ! = null )
2022-03-23 23:08:01 +08:00
{
2021-10-24 22:43:37 +08:00
// stream will be closed after use by LineBufferedReader.
2021-10-22 13:41:59 +08:00
ParseConfigurationStream ( configurationStream ) ;
2022-03-23 23:08:01 +08:00
Debug . Assert ( Configuration ! = null ) ;
}
2021-10-22 13:41:59 +08:00
else
Configuration = new SkinConfiguration ( ) ;
2021-05-10 21:43:48 +08:00
2021-11-29 17:02:09 +08:00
// skininfo files may be null for default skin.
2022-12-27 03:36:39 +08:00
foreach ( GlobalSkinComponentLookup . LookupType skinnableTarget in Enum . GetValues < GlobalSkinComponentLookup . LookupType > ( ) )
2021-05-11 10:54:45 +08:00
{
2022-03-22 17:36:42 +08:00
string filename = $"{skinnableTarget}.json" ;
2021-05-11 10:54:45 +08:00
2022-03-23 23:08:01 +08:00
byte [ ] ? bytes = storage ? . Get ( filename ) ;
2021-05-11 10:54:45 +08:00
2022-03-22 17:36:42 +08:00
if ( bytes = = null )
continue ;
2021-05-11 10:54:45 +08:00
2022-03-22 17:36:42 +08:00
try
{
string jsonContent = Encoding . UTF8 . GetString ( bytes ) ;
2022-07-31 02:25:38 +08:00
// handle namespace changes...
// can be removed 2023-01-31
jsonContent = jsonContent . Replace ( @"osu.Game.Screens.Play.SongProgress" , @"osu.Game.Screens.Play.HUD.DefaultSongProgress" ) ;
jsonContent = jsonContent . Replace ( @"osu.Game.Screens.Play.HUD.LegacyComboCounter" , @"osu.Game.Skinning.LegacyComboCounter" ) ;
2022-03-22 17:36:42 +08:00
var deserializedContent = JsonConvert . DeserializeObject < IEnumerable < SkinnableInfo > > ( jsonContent ) ;
2021-05-11 10:54:45 +08:00
2022-03-22 17:36:42 +08:00
if ( deserializedContent = = null )
2021-11-29 17:02:09 +08:00
continue ;
2021-05-11 10:54:45 +08:00
2022-03-22 17:36:42 +08:00
DrawableComponentInfo [ skinnableTarget ] = deserializedContent . ToArray ( ) ;
}
catch ( Exception ex )
{
Logger . Error ( ex , "Failed to load skin configuration." ) ;
2021-10-01 21:15:10 +08:00
}
2022-03-22 17:36:42 +08:00
}
2021-05-11 10:54:45 +08:00
}
2021-10-22 13:41:59 +08:00
protected virtual void ParseConfigurationStream ( Stream stream )
{
using ( LineBufferedReader reader = new LineBufferedReader ( stream , true ) )
Configuration = new LegacySkinDecoder ( ) . Decode ( reader ) ;
}
2021-05-13 12:09:33 +08:00
/// <summary>
/// Remove all stored customisations for the provided target.
/// </summary>
/// <param name="targetContainer">The target container to reset.</param>
2021-05-13 16:25:51 +08:00
public void ResetDrawableTarget ( ISkinnableTarget targetContainer )
2021-05-11 10:54:45 +08:00
{
DrawableComponentInfo . Remove ( targetContainer . Target ) ;
2021-05-10 21:43:48 +08:00
}
2021-05-13 12:09:33 +08:00
/// <summary>
/// Update serialised information for the provided target.
/// </summary>
/// <param name="targetContainer">The target container to serialise to this skin.</param>
2021-05-13 16:25:51 +08:00
public void UpdateDrawableTarget ( ISkinnableTarget targetContainer )
2021-05-10 21:43:48 +08:00
{
2021-05-13 12:14:49 +08:00
DrawableComponentInfo [ targetContainer . Target ] = targetContainer . CreateSkinnableInfo ( ) . ToArray ( ) ;
2021-05-10 21:43:48 +08:00
}
2022-11-09 15:04:56 +08:00
public virtual Drawable ? GetDrawableComponent ( ISkinComponentLookup lookup )
2021-05-10 21:43:48 +08:00
{
2022-11-09 13:11:41 +08:00
switch ( lookup )
2021-05-10 21:43:48 +08:00
{
2022-11-13 11:46:20 +08:00
// This fallback is important for user skins which use SkinnableSprites.
case SkinnableSprite . SpriteComponentLookup sprite :
return this . GetAnimation ( sprite . LookupName , false , false ) ;
2022-11-09 15:04:56 +08:00
case GlobalSkinComponentLookup target :
2022-11-09 15:03:29 +08:00
if ( ! DrawableComponentInfo . TryGetValue ( target . Lookup , out var skinnableInfo ) )
2021-05-10 21:43:48 +08:00
return null ;
2022-03-25 17:31:23 +08:00
var components = new List < Drawable > ( ) ;
foreach ( var i in skinnableInfo )
2022-04-01 13:30:02 +08:00
components . Add ( i . CreateInstance ( ) ) ;
2022-03-25 17:31:23 +08:00
2021-05-13 17:51:23 +08:00
return new SkinnableTargetComponentsContainer
2021-05-10 21:43:48 +08:00
{
2022-03-25 17:31:23 +08:00
Children = components ,
2021-05-10 21:43:48 +08:00
} ;
}
return null ;
2018-02-22 16:16:48 +08:00
}
2018-04-13 17:19:50 +08:00
2018-03-14 19:45:04 +08:00
#region Disposal
2018-04-13 17:19:50 +08:00
2018-03-14 19:45:04 +08:00
~ Skin ( )
{
2021-03-02 15:07:51 +08:00
// required to potentially clean up sample store from audio hierarchy.
2018-03-14 19:45:04 +08:00
Dispose ( false ) ;
}
2018-04-13 17:19:50 +08:00
2018-03-14 19:45:04 +08:00
public void Dispose ( )
{
Dispose ( true ) ;
GC . SuppressFinalize ( this ) ;
}
2018-04-13 17:19:50 +08:00
2018-03-14 19:45:04 +08:00
private bool isDisposed ;
2018-04-13 17:19:50 +08:00
2018-03-14 19:45:04 +08:00
protected virtual void Dispose ( bool isDisposing )
{
if ( isDisposed )
return ;
2019-02-28 12:31:40 +08:00
2018-03-14 19:45:04 +08:00
isDisposed = true ;
2022-03-22 17:36:42 +08:00
Textures ? . Dispose ( ) ;
Samples ? . Dispose ( ) ;
2022-03-24 21:53:49 +08:00
realmBackedStorage ? . Dispose ( ) ;
2018-03-14 19:45:04 +08:00
}
2018-04-13 17:19:50 +08:00
2018-03-14 19:45:04 +08:00
#endregion
2023-01-24 14:48:57 +08:00
public class SquishingTextureLoaderStore : IResourceStore < TextureUpload >
{
private readonly IResourceStore < TextureUpload > textureStore ;
public SquishingTextureLoaderStore ( IResourceStore < TextureUpload > textureStore )
{
this . textureStore = textureStore ;
}
public void Dispose ( )
{
textureStore . Dispose ( ) ;
}
public TextureUpload Get ( string name )
{
var textureUpload = textureStore . Get ( name ) ;
// NRT not enabled on framework side classes (IResourceStore / TextureLoaderStore), welp.
if ( textureUpload . IsNull ( ) )
return null ! ;
// So there's a thing where some users have taken it upon themselves to create skin elements of insane dimensions.
// To the point where GPUs cannot load the textures (along with most image editor apps).
// To work around this, let's look out for any stupid images and shrink them down into a usable size.
const int max_supported_texture_size = 16384 ;
if ( textureUpload . Height > max_supported_texture_size | | textureUpload . Width > max_supported_texture_size )
{
var image = Image . LoadPixelData ( textureUpload . Data . ToArray ( ) , textureUpload . Width , textureUpload . Height ) ;
2023-01-24 14:56:10 +08:00
// The original texture upload will no longer be returned or used.
textureUpload . Dispose ( ) ;
2023-01-24 14:48:57 +08:00
image . Mutate ( i = > i . Resize ( new Size (
Math . Min ( textureUpload . Width , max_supported_texture_size ) ,
Math . Min ( textureUpload . Height , max_supported_texture_size )
) ) ) ;
return new TextureUpload ( image ) ;
}
return textureUpload ;
}
public Task < TextureUpload > GetAsync ( string name , CancellationToken cancellationToken = new CancellationToken ( ) ) = > textureStore . GetAsync ( name , cancellationToken ) ;
public Stream GetStream ( string name ) = > textureStore . GetStream ( name ) ;
public IEnumerable < string > GetAvailableResources ( ) = > textureStore . GetAvailableResources ( ) ;
}
2018-02-22 16:16:48 +08:00
}
}