2019-09-03 08:57:34 +00:00
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
2019-01-24 08:43:03 +00:00
// See the LICENCE file in the repository root for full licence text.
2018-04-13 09:19:50 +00:00
2018-03-14 11:45:04 +00:00
using System ;
2021-05-10 13:43:48 +00:00
using System.Collections.Generic ;
2022-03-23 15:08:01 +00:00
using System.Diagnostics ;
2021-10-22 05:41:59 +00:00
using System.IO ;
2021-05-10 13:43:48 +00:00
using System.Linq ;
using System.Text ;
using Newtonsoft.Json ;
2018-02-22 08:16:48 +00:00
using osu.Framework.Audio.Sample ;
2019-09-03 08:57:34 +00:00
using osu.Framework.Bindables ;
2018-02-22 08:16:48 +00:00
using osu.Framework.Graphics ;
2018-03-20 07:28:39 +00:00
using osu.Framework.Graphics.Textures ;
2022-03-22 10:07:05 +00:00
using osu.Framework.IO.Stores ;
2021-10-01 13:15:10 +00:00
using osu.Framework.Logging ;
2019-08-23 11:32:43 +00:00
using osu.Game.Audio ;
2021-11-29 09:02:09 +00:00
using osu.Game.Database ;
2021-05-10 13:43:48 +00:00
using osu.Game.IO ;
using osu.Game.Screens.Play.HUD ;
2018-04-13 09:19:50 +00:00
2018-02-22 08:16:48 +00:00
namespace osu.Game.Skinning
{
2019-04-25 08:36:17 +00:00
public abstract class Skin : IDisposable , ISkin
2018-02-22 08:16:48 +00:00
{
2022-03-22 09:36:42 +00:00
/// <summary>
2022-03-24 03:39:47 +00:00
/// A texture store which can be used to perform user file lookups for this skin.
2022-03-22 09:36:42 +00:00
/// </summary>
2023-01-25 04:21:44 +00:00
protected TextureStore ? Textures { get ; }
2022-03-22 09:36:42 +00:00
/// <summary>
2022-03-24 03:39:47 +00:00
/// A sample store which can be used to perform user file lookups for this skin.
2022-03-22 09:36:42 +00:00
/// </summary>
2023-01-25 04:21:44 +00:00
protected ISampleStore ? Samples { get ; }
2022-03-22 09:36:42 +00:00
2022-01-26 04:37:33 +00:00
public readonly Live < SkinInfo > SkinInfo ;
2018-04-13 09:19:50 +00:00
2021-10-22 05:41:59 +00:00
public SkinConfiguration Configuration { get ; set ; }
2018-04-13 09:19:50 +00:00
2022-11-09 07:04:56 +00:00
public IDictionary < GlobalSkinComponentLookup . LookupType , SkinnableInfo [ ] > DrawableComponentInfo = > drawableComponentInfo ;
2021-05-10 13:43:48 +00:00
2022-11-09 07:04:56 +00:00
private readonly Dictionary < GlobalSkinComponentLookup . LookupType , SkinnableInfo [ ] > drawableComponentInfo = new Dictionary < GlobalSkinComponentLookup . LookupType , SkinnableInfo [ ] > ( ) ;
2018-04-13 09:19:50 +00:00
2023-01-25 04:21:44 +00:00
public abstract ISample ? GetSample ( ISampleInfo sampleInfo ) ;
2018-04-13 09:19:50 +00:00
2023-01-25 04:21:44 +00:00
public Texture ? GetTexture ( string componentName ) = > GetTexture ( componentName , default , default ) ;
2020-07-17 07:54:30 +00:00
2023-01-25 04:21:44 +00:00
public abstract Texture ? GetTexture ( string componentName , WrapMode wrapModeS , WrapMode wrapModeT ) ;
2018-04-13 09:19:50 +00:00
2023-01-25 04:21:44 +00:00
public abstract IBindable < TValue > ? GetConfig < TLookup , TValue > ( TLookup lookup )
2022-03-25 06:53:55 +00:00
where TLookup : notnull
where TValue : notnull ;
2018-04-13 09:19:50 +00:00
2023-01-25 04:21:44 +00:00
private readonly RealmBackedResourceStore < SkinInfo > ? realmBackedStorage ;
2022-03-24 10:09:17 +00:00
2022-03-23 04:14:56 +00: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 03:45:11 +00: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 05:53:00 +00: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 04:21:44 +00:00
protected Skin ( SkinInfo skin , IStorageResourceProvider ? resources , IResourceStore < byte [ ] > ? storage = null , string configurationFilename = @"skin.ini" )
2018-02-22 08:16:48 +00:00
{
2022-03-22 10:07:05 +00:00
if ( resources ! = null )
2022-03-22 09:36:42 +00:00
{
2022-03-23 15:08:01 +00:00
SkinInfo = skin . ToLive ( resources . RealmAccess ) ;
2022-03-23 05:21:35 +00:00
2022-03-24 10:09:17 +00:00
storage ? ? = realmBackedStorage = new RealmBackedResourceStore < SkinInfo > ( SkinInfo , resources . Files , resources . RealmAccess ) ;
2022-03-23 05:21:35 +00:00
var samples = resources . AudioManager ? . GetSampleStore ( storage ) ;
2023-01-28 20:16:22 +00:00
2022-03-23 05:21:35 +00:00
if ( samples ! = null )
2023-01-28 20:16:22 +00:00
{
2022-03-23 05:21:35 +00:00
samples . PlaybackConcurrency = OsuGameBase . SAMPLE_CONCURRENCY ;
2023-01-28 20:16:22 +00: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.
samples . AddExtension ( @"ogg" ) ;
}
2022-08-04 05:48:12 +00:00
2022-03-23 05:21:35 +00:00
Samples = samples ;
2023-01-24 11:01:00 +00:00
Textures = new TextureStore ( resources . Renderer , new MaxDimensionLimitedTextureLoaderStore ( resources . CreateTextureLoaderStore ( storage ) ) ) ;
2022-03-22 09:36:42 +00:00
}
2022-03-23 15:08:01 +00:00
else
{
// Generally only used for tests.
SkinInfo = skin . ToLiveUnmanaged ( ) ;
}
2022-03-22 09:36:42 +00:00
2022-03-23 05:53:00 +00:00
var configurationStream = storage ? . GetStream ( configurationFilename ) ;
2021-10-22 05:41:59 +00:00
if ( configurationStream ! = null )
2022-03-23 15:08:01 +00:00
{
2021-10-24 14:43:37 +00:00
// stream will be closed after use by LineBufferedReader.
2021-10-22 05:41:59 +00:00
ParseConfigurationStream ( configurationStream ) ;
2022-03-23 15:08:01 +00:00
Debug . Assert ( Configuration ! = null ) ;
}
2021-10-22 05:41:59 +00:00
else
Configuration = new SkinConfiguration ( ) ;
2021-05-10 13:43:48 +00:00
2021-11-29 09:02:09 +00:00
// skininfo files may be null for default skin.
2022-12-26 19:36:39 +00:00
foreach ( GlobalSkinComponentLookup . LookupType skinnableTarget in Enum . GetValues < GlobalSkinComponentLookup . LookupType > ( ) )
2021-05-11 02:54:45 +00:00
{
2022-03-22 09:36:42 +00:00
string filename = $"{skinnableTarget}.json" ;
2021-05-11 02:54:45 +00:00
2023-01-25 04:21:44 +00:00
byte [ ] ? bytes = storage ? . Get ( filename ) ;
2021-05-11 02:54:45 +00:00
2022-03-22 09:36:42 +00:00
if ( bytes = = null )
continue ;
2021-05-11 02:54:45 +00:00
2022-03-22 09:36:42 +00:00
try
{
string jsonContent = Encoding . UTF8 . GetString ( bytes ) ;
2022-07-30 18:25:38 +00: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 09:36:42 +00:00
var deserializedContent = JsonConvert . DeserializeObject < IEnumerable < SkinnableInfo > > ( jsonContent ) ;
2021-05-11 02:54:45 +00:00
2022-03-22 09:36:42 +00:00
if ( deserializedContent = = null )
2021-11-29 09:02:09 +00:00
continue ;
2021-05-11 02:54:45 +00:00
2022-03-22 09:36:42 +00:00
DrawableComponentInfo [ skinnableTarget ] = deserializedContent . ToArray ( ) ;
}
catch ( Exception ex )
{
Logger . Error ( ex , "Failed to load skin configuration." ) ;
2021-10-01 13:15:10 +00:00
}
2022-03-22 09:36:42 +00:00
}
2021-05-11 02:54:45 +00:00
}
2021-10-22 05:41:59 +00:00
protected virtual void ParseConfigurationStream ( Stream stream )
{
using ( LineBufferedReader reader = new LineBufferedReader ( stream , true ) )
Configuration = new LegacySkinDecoder ( ) . Decode ( reader ) ;
}
2021-05-13 04:09:33 +00:00
/// <summary>
/// Remove all stored customisations for the provided target.
/// </summary>
/// <param name="targetContainer">The target container to reset.</param>
2021-05-13 08:25:51 +00:00
public void ResetDrawableTarget ( ISkinnableTarget targetContainer )
2021-05-11 02:54:45 +00:00
{
DrawableComponentInfo . Remove ( targetContainer . Target ) ;
2021-05-10 13:43:48 +00:00
}
2021-05-13 04:09:33 +00: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 08:25:51 +00:00
public void UpdateDrawableTarget ( ISkinnableTarget targetContainer )
2021-05-10 13:43:48 +00:00
{
2021-05-13 04:14:49 +00:00
DrawableComponentInfo [ targetContainer . Target ] = targetContainer . CreateSkinnableInfo ( ) . ToArray ( ) ;
2021-05-10 13:43:48 +00:00
}
2023-01-25 04:21:44 +00:00
public virtual Drawable ? GetDrawableComponent ( ISkinComponentLookup lookup )
2021-05-10 13:43:48 +00:00
{
2022-11-09 05:11:41 +00:00
switch ( lookup )
2021-05-10 13:43:48 +00:00
{
2022-11-13 03:46:20 +00: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 07:04:56 +00:00
case GlobalSkinComponentLookup target :
2022-11-09 07:03:29 +00:00
if ( ! DrawableComponentInfo . TryGetValue ( target . Lookup , out var skinnableInfo ) )
2021-05-10 13:43:48 +00:00
return null ;
2022-03-25 09:31:23 +00:00
var components = new List < Drawable > ( ) ;
foreach ( var i in skinnableInfo )
2022-04-01 05:30:02 +00:00
components . Add ( i . CreateInstance ( ) ) ;
2022-03-25 09:31:23 +00:00
2021-05-13 09:51:23 +00:00
return new SkinnableTargetComponentsContainer
2021-05-10 13:43:48 +00:00
{
2022-03-25 09:31:23 +00:00
Children = components ,
2021-05-10 13:43:48 +00:00
} ;
}
return null ;
2018-02-22 08:16:48 +00:00
}
2018-04-13 09:19:50 +00:00
2018-03-14 11:45:04 +00:00
#region Disposal
2018-04-13 09:19:50 +00:00
2018-03-14 11:45:04 +00:00
~ Skin ( )
{
2021-03-02 07:07:51 +00:00
// required to potentially clean up sample store from audio hierarchy.
2018-03-14 11:45:04 +00:00
Dispose ( false ) ;
}
2018-04-13 09:19:50 +00:00
2018-03-14 11:45:04 +00:00
public void Dispose ( )
{
Dispose ( true ) ;
GC . SuppressFinalize ( this ) ;
}
2018-04-13 09:19:50 +00:00
2018-03-14 11:45:04 +00:00
private bool isDisposed ;
2018-04-13 09:19:50 +00:00
2018-03-14 11:45:04 +00:00
protected virtual void Dispose ( bool isDisposing )
{
if ( isDisposed )
return ;
2019-02-28 04:31:40 +00:00
2018-03-14 11:45:04 +00:00
isDisposed = true ;
2022-03-22 09:36:42 +00:00
Textures ? . Dispose ( ) ;
Samples ? . Dispose ( ) ;
2022-03-24 13:53:49 +00:00
realmBackedStorage ? . Dispose ( ) ;
2018-03-14 11:45:04 +00:00
}
2018-04-13 09:19:50 +00:00
2018-03-14 11:45:04 +00:00
#endregion
2018-02-22 08:16:48 +00:00
}
}