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
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 ;
using Newtonsoft.Json ;
2018-04-13 17:19:50 +08:00
using osu.Framework.Audio.Sample ;
2019-09-03 16:57:34 +08:00
using osu.Framework.Bindables ;
2018-04-13 17:19:50 +08:00
using osu.Framework.Graphics ;
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 ;
2018-04-13 17:19:50 +08:00
namespace osu.Game.Skinning
{
2019-04-25 16:36:17 +08:00
public abstract class Skin : IDisposable , ISkin
2018-04-13 17:19:50 +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>
2023-01-25 12:21:44 +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>
2023-01-25 12:21:44 +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
2023-01-25 12:21:44 +08:00
public abstract ISample ? GetSample ( ISampleInfo sampleInfo ) ;
2018-04-13 17:19:50 +08:00
2023-01-25 12:21:44 +08:00
public Texture ? GetTexture ( string componentName ) = > GetTexture ( componentName , default , default ) ;
2020-07-17 15:54:30 +08:00
2023-01-25 12:21:44 +08:00
public abstract Texture ? GetTexture ( string componentName , WrapMode wrapModeS , WrapMode wrapModeT ) ;
2018-04-13 17:19:50 +08:00
2023-01-25 12:21:44 +08:00
public abstract IBindable < TValue > ? GetConfig < TLookup , TValue > ( TLookup lookup )
2022-03-25 14:53:55 +08:00
where TLookup : notnull
where TValue : notnull ;
2018-04-13 17:19:50 +08:00
2023-01-25 12:21:44 +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>
2023-01-25 12:21:44 +08:00
protected Skin ( SkinInfo skin , IStorageResourceProvider ? resources , IResourceStore < byte [ ] > ? storage = null , string configurationFilename = @"skin.ini" )
2018-04-13 17:19:50 +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 19:01:00 +08:00
Textures = new TextureStore ( resources . Renderer , new MaxDimensionLimitedTextureLoaderStore ( 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
2023-01-25 12:21:44 +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
}
2023-01-25 12:21:44 +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-04-13 17:19:50 +08:00
}
#region Disposal
~ Skin ( )
{
2021-03-02 15:07:51 +08:00
// required to potentially clean up sample store from audio hierarchy.
2018-04-13 17:19:50 +08:00
Dispose ( false ) ;
}
public void Dispose ( )
{
Dispose ( true ) ;
GC . SuppressFinalize ( this ) ;
}
private bool isDisposed ;
protected virtual void Dispose ( bool isDisposing )
{
if ( isDisposed )
return ;
2019-02-28 12:31:40 +08:00
2018-04-13 17:19:50 +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-04-13 17:19:50 +08:00
}
#endregion
}
}