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 ;
2023-09-06 16:37:17 +08:00
using System.Runtime.CompilerServices ;
2021-05-10 21:43:48 +08:00
using System.Text ;
2023-09-06 16:37:17 +08:00
using System.Threading ;
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-09-06 16:37:17 +08:00
using osu.Framework.Extensions.TypeExtensions ;
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 ;
2024-07-01 11:48:05 +08:00
using osu.Game.Rulesets ;
using osu.Game.Screens.Play.HUD ;
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
{
2024-07-01 11:48:05 +08:00
private readonly IStorageResourceProvider ? resources ;
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
2024-08-22 17:45:44 +08:00
public IDictionary < GlobalSkinnableContainers , SkinLayoutInfo > LayoutInfos = > layoutInfos ;
2021-05-10 21:43:48 +08:00
2024-08-22 17:45:44 +08:00
private readonly Dictionary < GlobalSkinnableContainers , SkinLayoutInfo > layoutInfos =
new Dictionary < GlobalSkinnableContainers , SkinLayoutInfo > ( ) ;
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-09-19 08:17:07 +08:00
public Texture ? GetTexture ( string componentName ) = > GetTexture ( componentName , default , default ) ;
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-11-16 19:16:23 +08:00
private readonly ResourceStore < byte [ ] > store = new ResourceStore < byte [ ] > ( ) ;
2022-03-24 18:09:17 +08:00
2023-09-06 16:37:17 +08:00
public string Name { get ; }
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>
2023-11-16 19:16:23 +08: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 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-11-16 19:16:23 +08:00
protected Skin ( SkinInfo skin , IStorageResourceProvider ? resources , IResourceStore < byte [ ] > ? fallbackStore = null , string configurationFilename = @"skin.ini" )
2018-02-22 16:16:48 +08:00
{
2024-07-01 11:48:05 +08:00
this . resources = resources ;
2023-09-06 16:37:17 +08:00
Name = skin . Name ;
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
2023-11-16 19:16:23 +08:00
store . AddStore ( new RealmBackedResourceStore < SkinInfo > ( SkinInfo , resources . Files , resources . RealmAccess ) ) ;
2022-03-24 18:09:17 +08:00
2023-11-16 19:16:23 +08:00
var samples = resources . AudioManager ? . GetSampleStore ( store ) ;
2023-01-29 04:16:22 +08:00
2022-03-23 13:21:35 +08:00
if ( samples ! = null )
2023-01-29 04:16:22 +08:00
{
2022-03-23 13:21:35 +08:00
samples . PlaybackConcurrency = OsuGameBase . SAMPLE_CONCURRENCY ;
2023-01-29 04:16:22 +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.
samples . AddExtension ( @"ogg" ) ;
}
2022-08-04 13:48:12 +08:00
2022-03-23 13:21:35 +08:00
Samples = samples ;
2023-11-16 19:16:23 +08:00
Textures = new TextureStore ( resources . Renderer , CreateTextureLoaderStore ( resources , store ) ) ;
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
2023-11-16 19:16:23 +08:00
if ( fallbackStore ! = null )
store . AddStore ( fallbackStore ) ;
var configurationStream = store . 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
2023-09-27 15:45:38 +08: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 21:43:48 +08:00
2021-11-29 17:02:09 +08:00
// skininfo files may be null for default skin.
2024-08-22 17:45:44 +08:00
foreach ( GlobalSkinnableContainers skinnableTarget in Enum . GetValues < GlobalSkinnableContainers > ( ) )
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-11-16 19:16:23 +08:00
byte [ ] ? bytes = store ? . 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
2024-07-01 11:48:05 +08:00
var layoutInfo = parseLayoutInfo ( jsonContent , skinnableTarget ) ;
2023-02-16 18:58:04 +08:00
if ( layoutInfo = = null )
2024-07-01 11:48:05 +08:00
continue ;
2023-02-16 18:58:04 +08:00
LayoutInfos [ skinnableTarget ] = layoutInfo ;
2022-03-22 17:36:42 +08:00
}
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
}
2023-09-30 06:17:59 +08:00
protected virtual IResourceStore < TextureUpload > CreateTextureLoaderStore ( IStorageResourceProvider resources , IResourceStore < byte [ ] > storage )
= > new MaxDimensionLimitedTextureLoaderStore ( resources . CreateTextureLoaderStore ( storage ) ) ;
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>
2024-08-22 16:14:35 +08:00
public void ResetDrawableTarget ( SkinnableContainer targetContainer )
2021-05-11 10:54:45 +08:00
{
2024-08-22 18:00:15 +08:00
LayoutInfos . Remove ( targetContainer . Lookup . Component ) ;
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>
2024-08-22 16:14:35 +08:00
public void UpdateDrawableTarget ( SkinnableContainer targetContainer )
2021-05-10 21:43:48 +08:00
{
2024-08-22 18:00:15 +08:00
if ( ! LayoutInfos . TryGetValue ( targetContainer . Lookup . Component , out var layoutInfo ) )
layoutInfos [ targetContainer . Lookup . Component ] = layoutInfo = new SkinLayoutInfo ( ) ;
2023-02-16 18:58:04 +08:00
layoutInfo . Update ( targetContainer . Lookup . Ruleset , ( ( ISerialisableDrawableContainer ) targetContainer ) . CreateSerialisedInfo ( ) . 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 :
2023-09-19 09:36:40 +08:00
return this . GetAnimation ( sprite . LookupName , false , false , maxSize : sprite . MaxSize ) ;
2022-11-13 11:46:20 +08:00
2024-08-22 16:14:35 +08:00
case UserSkinComponentLookup userLookup :
switch ( userLookup . Component )
2021-05-10 21:43:48 +08:00
{
2024-08-22 16:14:35 +08:00
case GlobalSkinnableContainerLookup containerLookup :
// 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 . Component , out var layoutInfo ) ) return null ;
if ( ! layoutInfo . TryGetDrawableInfo ( containerLookup . Ruleset , out var drawableInfos ) ) return null ;
return new UserConfiguredLayoutContainer
{
RelativeSizeAxes = Axes . Both ,
ChildrenEnumerable = drawableInfos . Select ( i = > i . CreateInstance ( ) )
} ;
}
break ;
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
2024-07-01 11:48:05 +08:00
#region Deserialisation & Migration
2024-08-22 17:45:44 +08:00
private SkinLayoutInfo ? parseLayoutInfo ( string jsonContent , GlobalSkinnableContainers target )
2024-07-01 11:48:05 +08:00
{
SkinLayoutInfo ? layout = null ;
// 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" ) ;
2024-08-08 23:19:17 +08:00
jsonContent = jsonContent . Replace ( @"osu.Game.Skinning.LegacyComboCounter" , @"osu.Game.Skinning.LegacyDefaultComboCounter" ) ;
2024-07-01 11:48:05 +08:00
jsonContent = jsonContent . Replace ( @"osu.Game.Screens.Play.HUD.PerformancePointsCounter" , @"osu.Game.Skinning.Triangles.TrianglesPerformancePointsCounter" ) ;
try
{
// First attempt to deserialise using the new SkinLayoutInfo format
layout = JsonConvert . DeserializeObject < SkinLayoutInfo > ( jsonContent ) ;
}
catch
{
}
// If deserialisation using SkinLayoutInfo fails, attempt to deserialise using the old naked list.
if ( layout = = null )
{
var deserializedContent = JsonConvert . DeserializeObject < IEnumerable < SerialisedDrawableInfo > > ( jsonContent ) ;
if ( deserializedContent = = null )
return null ;
layout = new SkinLayoutInfo { Version = 0 } ;
layout . Update ( null , deserializedContent . ToArray ( ) ) ;
Logger . Log ( $"Ferrying {deserializedContent.Count()} components in {target} to global section of new {nameof(SkinLayoutInfo)} format" ) ;
}
for ( int i = layout . Version + 1 ; i < = SkinLayoutInfo . LATEST_VERSION ; i + + )
applyMigration ( layout , target , i ) ;
layout . Version = SkinLayoutInfo . LATEST_VERSION ;
return layout ;
}
2024-08-22 17:45:44 +08:00
private void applyMigration ( SkinLayoutInfo layout , GlobalSkinnableContainers target , int version )
2024-07-01 11:48:05 +08:00
{
switch ( version )
{
case 1 :
{
2024-08-09 16:26:54 +08:00
// Combo counters were moved out of the global HUD components into per-ruleset.
// This is to allow some rulesets to customise further (ie. mania and catch moving the combo to within their play area).
2024-08-22 17:45:44 +08:00
if ( target ! = GlobalSkinnableContainers . MainHUDComponents | |
2024-07-01 11:48:05 +08:00
! layout . TryGetDrawableInfo ( null , out var globalHUDComponents ) | |
resources = = null )
break ;
var comboCounters = globalHUDComponents . Where ( c = >
2024-08-09 16:26:54 +08:00
c . Type . Name = = nameof ( LegacyDefaultComboCounter ) | |
2024-07-01 11:48:05 +08:00
c . Type . Name = = nameof ( DefaultComboCounter ) | |
c . Type . Name = = nameof ( ArgonComboCounter ) ) . ToArray ( ) ;
layout . Update ( null , globalHUDComponents . Except ( comboCounters ) . ToArray ( ) ) ;
resources . RealmAccess . Run ( r = >
{
foreach ( var ruleset in r . All < RulesetInfo > ( ) )
{
layout . Update ( ruleset , layout . TryGetDrawableInfo ( ruleset , out var rulesetHUDComponents )
? rulesetHUDComponents . Concat ( comboCounters ) . ToArray ( )
: comboCounters ) ;
}
} ) ;
break ;
}
}
}
#endregion
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
2023-11-16 19:16:23 +08:00
store . 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-09-06 16:37:17 +08:00
2023-09-06 23:46:42 +08:00
public override string ToString ( ) = > $"{GetType().ReadableName()} {{ Name: {Name} }}" ;
2023-09-06 16:37:17 +08: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 00:23:56 +08:00
icon = "🟢 hit" ;
2023-09-06 16:37:17 +08:00
break ;
case LookupDebugType . Miss :
2023-09-07 00:23:56 +08:00
icon = "🔴 miss" ;
2023-09-06 16:37:17 +08: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 16:16:48 +08:00
}
}