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