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
2018-03-14 20:45:04 +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 ;
2023-09-06 17:37:17 +09:00
using System.Runtime.CompilerServices ;
2021-05-10 22:43:48 +09:00
using System.Text ;
2023-09-06 17:37:17 +09:00
using System.Threading ;
2021-05-10 22:43:48 +09:00
using Newtonsoft.Json ;
2018-02-22 17:16:48 +09:00
using osu.Framework.Audio.Sample ;
2019-09-03 17:57:34 +09:00
using osu.Framework.Bindables ;
2023-09-06 17:37:17 +09:00
using osu.Framework.Extensions.TypeExtensions ;
2018-02-22 17:16:48 +09:00
using osu.Framework.Graphics ;
2023-02-15 17:24:34 +09:00
using osu.Framework.Graphics.Containers ;
2018-03-20 16:28:39 +09:00
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 ;
2018-04-13 18:19:50 +09:00
2018-02-22 17:16:48 +09:00
namespace osu.Game.Skinning
{
2019-04-25 17:36:17 +09:00
public abstract class Skin : IDisposable , ISkin
2018-02-22 17:16:48 +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>
2023-01-25 13:21:44 +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>
2023-01-25 13:21:44 +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
2023-02-16 19:58:04 +09:00
public IDictionary < SkinComponentsContainerLookup . TargetArea , SkinLayoutInfo > LayoutInfos = > layoutInfos ;
2021-05-10 22:43:48 +09:00
2023-02-16 19:58:04 +09:00
private readonly Dictionary < SkinComponentsContainerLookup . TargetArea , SkinLayoutInfo > layoutInfos =
new Dictionary < SkinComponentsContainerLookup . TargetArea , SkinLayoutInfo > ( ) ;
2018-04-13 18:19:50 +09:00
2023-01-25 13:21:44 +09:00
public abstract ISample ? GetSample ( ISampleInfo sampleInfo ) ;
2018-04-13 18:19:50 +09:00
2023-09-19 03:17:07 +03:00
public Texture ? GetTexture ( string componentName ) = > GetTexture ( componentName , default , default ) ;
public abstract Texture ? GetTexture ( string componentName , WrapMode wrapModeS , WrapMode wrapModeT ) ;
2018-04-13 18:19:50 +09:00
2023-01-25 13:21:44 +09:00
public abstract IBindable < TValue > ? GetConfig < TLookup , TValue > ( TLookup lookup )
2022-03-25 15:53:55 +09:00
where TLookup : notnull
where TValue : notnull ;
2018-04-13 18:19:50 +09:00
2023-11-16 20:16:23 +09:00
private readonly ResourceStore < byte [ ] > store = new ResourceStore < byte [ ] > ( ) ;
2022-03-24 19:09:17 +09:00
2023-09-06 17:37:17 +09:00
public string Name { get ; }
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>
2023-11-16 20:16:23 +09:00
/// <param name="fallbackStore">An optional fallback store which will be used for file lookups that are not serviced by realm user storage.</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>
2023-11-16 20:16:23 +09:00
protected Skin ( SkinInfo skin , IStorageResourceProvider ? resources , IResourceStore < byte [ ] > ? fallbackStore = null , string configurationFilename = @"skin.ini" )
2018-02-22 17:16:48 +09:00
{
2023-09-06 17:37:17 +09:00
Name = skin . Name ;
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
2023-11-16 20:16:23 +09:00
store . AddStore ( new RealmBackedResourceStore < SkinInfo > ( SkinInfo , resources . Files , resources . RealmAccess ) ) ;
2022-03-24 19:09:17 +09:00
2023-11-16 20:16:23 +09:00
var samples = resources . AudioManager ? . GetSampleStore ( store ) ;
2023-01-28 21:16:22 +01:00
2022-03-23 14:21:35 +09:00
if ( samples ! = null )
2023-01-28 21:16:22 +01:00
{
2022-03-23 14:21:35 +09:00
samples . PlaybackConcurrency = OsuGameBase . SAMPLE_CONCURRENCY ;
2023-01-28 21:16:22 +01: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 14:48:12 +09:00
2022-03-23 14:21:35 +09:00
Samples = samples ;
2023-11-16 20:16:23 +09:00
Textures = new TextureStore ( resources . Renderer , CreateTextureLoaderStore ( resources , store ) ) ;
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
2023-11-16 20:16:23 +09:00
if ( fallbackStore ! = null )
store . AddStore ( fallbackStore ) ;
var configurationStream = store . 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
2023-09-27 16:45:38 +09:00
{
Configuration = new SkinConfiguration
{
// generally won't be hit as we always write a `skin.ini` on import, but best be safe than sorry.
// see https://github.com/peppy/osu-stable-reference/blob/1531237b63392e82c003c712faa028406073aa8f/osu!/Graphics/Skinning/SkinManager.cs#L297-L298
LegacyVersion = SkinConfiguration . LATEST_VERSION ,
} ;
}
2021-05-10 22:43:48 +09:00
2021-11-29 18:02:09 +09:00
// skininfo files may be null for default skin.
2023-02-15 18:31:55 +09:00
foreach ( SkinComponentsContainerLookup . TargetArea skinnableTarget in Enum . GetValues < SkinComponentsContainerLookup . TargetArea > ( ) )
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
2023-11-16 20:16:23 +09:00
byte [ ] ? bytes = store ? . 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
2023-02-16 19:58:04 +09:00
SkinLayoutInfo ? layoutInfo = null ;
2022-07-31 03:25:38 +09:00
2024-03-08 10:26:08 +08:00
// handle namespace changes...
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" ) ;
jsonContent = jsonContent . Replace ( @"osu.Game.Screens.Play.HUD.PerformancePointsCounter" , @"osu.Game.Skinning.Triangles.TrianglesPerformancePointsCounter" ) ;
2023-02-16 19:58:04 +09:00
try
{
// First attempt to deserialise using the new SkinLayoutInfo format
layoutInfo = JsonConvert . DeserializeObject < SkinLayoutInfo > ( jsonContent ) ;
}
catch
{
}
2023-02-20 19:51:54 +09:00
// Of note, the migration code below runs on read of skins, but there's nothing to
// force a rewrite after migration. Let's not remove these migration rules until we
// have something in place to ensure we don't end up breaking skins of users that haven't
// manually saved their skin since a change was implemented.
2023-02-20 20:07:17 +01:00
// If deserialisation using SkinLayoutInfo fails, attempt to deserialise using the old naked list.
2023-02-16 19:58:04 +09:00
if ( layoutInfo = = null )
{
var deserializedContent = JsonConvert . DeserializeObject < IEnumerable < SerialisedDrawableInfo > > ( jsonContent ) ;
2021-05-11 11:54:45 +09:00
2023-02-16 19:58:04 +09:00
if ( deserializedContent = = null )
continue ;
2021-05-11 11:54:45 +09:00
2023-02-16 19:58:04 +09:00
layoutInfo = new SkinLayoutInfo ( ) ;
layoutInfo . Update ( null , deserializedContent . ToArray ( ) ) ;
Logger . Log ( $"Ferrying {deserializedContent.Count()} components in {skinnableTarget} to global section of new {nameof(SkinLayoutInfo)} format" ) ;
}
LayoutInfos [ skinnableTarget ] = layoutInfo ;
2022-03-22 18:36:42 +09:00
}
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
}
2023-09-30 01:17:59 +03:00
protected virtual IResourceStore < TextureUpload > CreateTextureLoaderStore ( IStorageResourceProvider resources , IResourceStore < byte [ ] > storage )
= > new MaxDimensionLimitedTextureLoaderStore ( resources . CreateTextureLoaderStore ( storage ) ) ;
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>
2023-02-15 18:31:55 +09:00
public void ResetDrawableTarget ( SkinComponentsContainer targetContainer )
2021-05-11 11:54:45 +09:00
{
2023-02-16 19:58:04 +09:00
LayoutInfos . Remove ( targetContainer . Lookup . 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>
2023-02-15 18:31:55 +09:00
public void UpdateDrawableTarget ( SkinComponentsContainer targetContainer )
2021-05-10 22:43:48 +09:00
{
2023-02-16 19:58:04 +09:00
if ( ! LayoutInfos . TryGetValue ( targetContainer . Lookup . Target , out var layoutInfo ) )
layoutInfos [ targetContainer . Lookup . Target ] = layoutInfo = new SkinLayoutInfo ( ) ;
layoutInfo . Update ( targetContainer . Lookup . Ruleset , ( ( ISerialisableDrawableContainer ) targetContainer ) . CreateSerialisedInfo ( ) . ToArray ( ) ) ;
2021-05-10 22:43:48 +09:00
}
2023-01-25 13:21:44 +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 :
2023-09-19 04:36:40 +03:00
return this . GetAnimation ( sprite . LookupName , false , false , maxSize : sprite . MaxSize ) ;
2022-11-13 12:46:20 +09:00
2023-02-16 15:33:56 +09:00
case SkinComponentsContainerLookup containerLookup :
2022-03-25 18:31:23 +09:00
2023-02-16 19:58:04 +09:00
// It is important to return null if the user has not configured this yet.
// This allows skin transformers the opportunity to provide default components.
if ( ! LayoutInfos . TryGetValue ( containerLookup . Target , out var layoutInfo ) ) return null ;
if ( ! layoutInfo . TryGetDrawableInfo ( containerLookup . Ruleset , out var drawableInfos ) ) return null ;
2022-03-25 18:31:23 +09:00
2023-02-15 17:24:34 +09:00
return new Container
2021-05-10 22:43:48 +09:00
{
2023-02-15 17:24:34 +09:00
RelativeSizeAxes = Axes . Both ,
2023-02-16 19:58:04 +09:00
ChildrenEnumerable = drawableInfos . Select ( i = > i . CreateInstance ( ) )
2021-05-10 22:43:48 +09:00
} ;
}
return null ;
2018-02-22 17:16:48 +09:00
}
2018-04-13 18:19:50 +09:00
2018-03-14 20:45:04 +09:00
#region Disposal
2018-04-13 18:19:50 +09:00
2018-03-14 20:45:04 +09:00
~ Skin ( )
{
2021-03-02 16:07:51 +09:00
// required to potentially clean up sample store from audio hierarchy.
2018-03-14 20:45:04 +09:00
Dispose ( false ) ;
}
2018-04-13 18:19:50 +09:00
2018-03-14 20:45:04 +09:00
public void Dispose ( )
{
Dispose ( true ) ;
GC . SuppressFinalize ( this ) ;
}
2018-04-13 18:19:50 +09:00
2018-03-14 20:45:04 +09:00
private bool isDisposed ;
2018-04-13 18:19:50 +09:00
2018-03-14 20:45:04 +09:00
protected virtual void Dispose ( bool isDisposing )
{
if ( isDisposed )
return ;
2019-02-28 13:31:40 +09:00
2018-03-14 20:45:04 +09:00
isDisposed = true ;
2022-03-22 18:36:42 +09:00
Textures ? . Dispose ( ) ;
Samples ? . Dispose ( ) ;
2022-03-24 22:53:49 +09:00
2023-11-16 20:16:23 +09:00
store . Dispose ( ) ;
2018-03-14 20:45:04 +09:00
}
2018-04-13 18:19:50 +09:00
2018-03-14 20:45:04 +09:00
#endregion
2023-09-06 17:37:17 +09:00
2023-09-06 17:46:42 +02:00
public override string ToString ( ) = > $"{GetType().ReadableName()} {{ Name: {Name} }}" ;
2023-09-06 17:37:17 +09:00
private static readonly ThreadLocal < int > nested_level = new ThreadLocal < int > ( ( ) = > 0 ) ;
[Conditional("SKIN_LOOKUP_DEBUG")]
internal static void LogLookupDebug ( object callingClass , object lookup , LookupDebugType type , [ CallerMemberName ] string callerMethod = "" )
{
string icon = string . Empty ;
int level = nested_level . Value ;
switch ( type )
{
case LookupDebugType . Hit :
2023-09-07 01:23:56 +09:00
icon = "🟢 hit" ;
2023-09-06 17:37:17 +09:00
break ;
case LookupDebugType . Miss :
2023-09-07 01:23:56 +09:00
icon = "🔴 miss" ;
2023-09-06 17:37:17 +09:00
break ;
case LookupDebugType . Enter :
nested_level . Value + + ;
break ;
case LookupDebugType . Exit :
nested_level . Value - - ;
if ( nested_level . Value = = 0 )
Logger . Log ( string . Empty ) ;
return ;
}
string lookupString = lookup . ToString ( ) ? ? string . Empty ;
string callingClassString = callingClass . ToString ( ) ? ? string . Empty ;
Logger . Log ( $"{string.Join(null, Enumerable.Repeat(" | - ", level))}{callingClassString}.{callerMethod}(lookup: {lookupString}) {icon}" ) ;
}
internal enum LookupDebugType
{
Hit ,
Miss ,
Enter ,
Exit
}
2018-02-22 17:16:48 +09:00
}
}