2019-01-24 16:43:03 +08:00
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
2018-04-13 17:19:50 +08:00
using System ;
using System.Collections.Generic ;
using System.IO ;
using System.Linq ;
using System.Reflection ;
2020-05-31 07:18:07 +08:00
using osu.Framework ;
2023-06-24 08:48:16 +08:00
using osu.Framework.Extensions.ObjectExtensions ;
2018-06-02 23:28:29 +08:00
using osu.Framework.Logging ;
2020-04-03 23:32:37 +08:00
using osu.Framework.Platform ;
2018-04-13 17:19:50 +08:00
namespace osu.Game.Rulesets
{
2022-02-16 15:57:28 +08:00
public abstract class RulesetStore : IDisposable , IRulesetStore
2018-04-13 17:19:50 +08:00
{
2021-11-23 12:00:33 +08:00
private const string ruleset_library_prefix = @"osu.Game.Rulesets" ;
2018-04-13 17:19:50 +08:00
2022-02-16 15:57:28 +08:00
protected readonly Dictionary < Assembly , Type > LoadedAssemblies = new Dictionary < Assembly , Type > ( ) ;
2024-03-23 02:05:58 +08:00
protected readonly HashSet < Assembly > UserRulesetAssemblies = new HashSet < Assembly > ( ) ;
protected readonly Storage ? RulesetStorage ;
2018-04-13 17:19:50 +08:00
2021-11-23 12:00:33 +08:00
/// <summary>
/// All available rulesets.
/// </summary>
2022-02-16 15:57:28 +08:00
public abstract IEnumerable < RulesetInfo > AvailableRulesets { get ; }
2020-04-03 23:32:37 +08:00
2022-02-16 15:57:28 +08:00
protected RulesetStore ( Storage ? storage = null )
2019-10-15 15:14:06 +08:00
{
2019-07-03 17:36:04 +08:00
// On android in release configuration assemblies are loaded from the apk directly into memory.
2019-07-03 17:41:01 +08:00
// We cannot read assemblies from cwd, so should check loaded assemblies instead.
2019-07-03 17:42:10 +08:00
loadFromAppDomain ( ) ;
2021-08-22 13:26:35 +08:00
2021-08-22 17:40:41 +08:00
// This null check prevents Android from attempting to load the rulesets from disk,
// as the underlying path "AppContext.BaseDirectory", despite being non-nullable, it returns null on android.
// See https://github.com/xamarin/xamarin-android/issues/3489.
2023-06-24 08:48:16 +08:00
if ( RuntimeInfo . StartupDirectory . IsNotNull ( ) )
2021-08-22 13:26:35 +08:00
loadFromDisk ( ) ;
2020-04-19 22:29:32 +08:00
2020-04-19 21:25:21 +08:00
// the event handler contains code for resolving dependency on the game assembly for rulesets located outside the base game directory.
// It needs to be attached to the assembly lookup event before the actual call to loadUserRulesets() else rulesets located out of the base game directory will fail
// to load as unable to locate the game core assembly.
2020-04-07 18:20:54 +08:00
AppDomain . CurrentDomain . AssemblyResolve + = resolveRulesetDependencyAssembly ;
2021-11-23 12:00:33 +08:00
2024-03-23 02:05:58 +08:00
RulesetStorage = storage ? . GetStorageForDirectory ( @"rulesets" ) ;
if ( RulesetStorage ! = null )
loadUserRulesets ( RulesetStorage ) ;
2018-04-13 17:19:50 +08:00
}
/// <summary>
/// Retrieve a ruleset using a known ID.
/// </summary>
/// <param name="id">The ruleset's internal ID.</param>
/// <returns>A ruleset, if available, else null.</returns>
2021-11-23 12:00:33 +08:00
public RulesetInfo ? GetRuleset ( int id ) = > AvailableRulesets . FirstOrDefault ( r = > r . OnlineID = = id ) ;
2018-04-13 17:19:50 +08:00
/// <summary>
/// Retrieve a ruleset using a known short name.
/// </summary>
/// <param name="shortName">The ruleset's short name.</param>
/// <returns>A ruleset, if available, else null.</returns>
2021-11-23 12:00:33 +08:00
public RulesetInfo ? GetRuleset ( string shortName ) = > AvailableRulesets . FirstOrDefault ( r = > r . ShortName = = shortName ) ;
2018-04-13 17:19:50 +08:00
2021-11-23 12:00:33 +08:00
private Assembly ? resolveRulesetDependencyAssembly ( object? sender , ResolveEventArgs args )
2020-04-03 23:32:37 +08:00
{
var asm = new AssemblyName ( args . Name ) ;
2020-04-07 18:20:54 +08:00
// the requesting assembly may be located out of the executable's base directory, thus requiring manual resolving of its dependencies.
2020-04-20 19:56:15 +08:00
// this attempts resolving the ruleset dependencies on game core and framework assemblies by returning assemblies with the same assembly name
// already loaded in the AppDomain.
2020-07-30 20:10:13 +08:00
var domainAssembly = AppDomain . CurrentDomain . GetAssemblies ( )
// Given name is always going to be equally-or-more qualified than the assembly name.
2021-11-23 12:00:33 +08:00
. Where ( a = >
{
string? name = a . GetName ( ) . Name ;
if ( name = = null )
return false ;
return args . Name . Contains ( name , StringComparison . Ordinal ) ;
2022-12-19 15:42:21 +08:00
} ) . MaxBy ( a = > a . GetName ( ) . Version ) ;
2020-07-30 20:10:13 +08:00
if ( domainAssembly ! = null )
return domainAssembly ;
2020-04-03 23:32:37 +08:00
2022-02-16 15:57:28 +08:00
return LoadedAssemblies . Keys . FirstOrDefault ( a = > a . FullName = = asm . FullName ) ;
2018-04-13 17:19:50 +08:00
}
2019-10-15 15:14:06 +08:00
private void loadFromAppDomain ( )
2019-07-02 23:05:04 +08:00
{
2019-07-02 23:25:12 +08:00
foreach ( var ruleset in AppDomain . CurrentDomain . GetAssemblies ( ) )
{
2021-11-23 12:00:33 +08:00
string? rulesetName = ruleset . GetName ( ) . Name ;
2019-07-02 23:25:12 +08:00
2021-11-23 12:00:33 +08:00
if ( rulesetName = = null )
continue ;
if ( ! rulesetName . StartsWith ( ruleset_library_prefix , StringComparison . InvariantCultureIgnoreCase ) | | rulesetName . Contains ( @"Tests" ) )
2019-07-02 23:25:12 +08:00
continue ;
2019-07-02 23:05:04 +08:00
addRuleset ( ruleset ) ;
2019-07-02 23:25:12 +08:00
}
2019-07-02 23:05:04 +08:00
}
2021-11-23 12:00:33 +08:00
private void loadUserRulesets ( Storage rulesetStorage )
2020-04-03 23:32:37 +08:00
{
2021-11-23 12:00:33 +08:00
var rulesets = rulesetStorage . GetFiles ( @"." , @ $"{ruleset_library_prefix}.*.dll" ) ;
2020-04-03 23:32:37 +08:00
2021-11-23 12:00:33 +08:00
foreach ( string? ruleset in rulesets . Where ( f = > ! f . Contains ( @"Tests" ) ) )
2024-03-23 02:05:58 +08:00
{
var assembly = loadRulesetFromFile ( rulesetStorage . GetFullPath ( ruleset ) ) ;
if ( assembly ! = null )
UserRulesetAssemblies . Add ( assembly ) ;
}
2020-04-03 23:32:37 +08:00
}
2019-10-15 15:14:06 +08:00
private void loadFromDisk ( )
2019-07-03 17:42:10 +08:00
{
try
{
2022-12-19 14:43:12 +08:00
// On net6-android (Debug), StartupDirectory can be different from where assemblies are placed.
// Search sub-directories too.
string [ ] files = Directory . GetFiles ( RuntimeInfo . StartupDirectory , @ $"{ruleset_library_prefix}.*.dll" , SearchOption . AllDirectories ) ;
2019-07-03 17:42:10 +08:00
foreach ( string file in files . Where ( f = > ! Path . GetFileName ( f ) . Contains ( "Tests" ) ) )
loadRulesetFromFile ( file ) ;
}
2019-10-01 14:41:01 +08:00
catch ( Exception e )
2019-07-03 17:42:10 +08:00
{
2020-05-31 07:18:07 +08:00
Logger . Error ( e , $"Could not load rulesets from directory {RuntimeInfo.StartupDirectory}" ) ;
2019-07-03 17:42:10 +08:00
}
}
2024-03-23 02:05:58 +08:00
private Assembly ? loadRulesetFromFile ( string file )
2018-04-13 17:19:50 +08:00
{
2022-12-16 17:16:26 +08:00
string filename = Path . GetFileNameWithoutExtension ( file ) ;
2018-04-13 17:19:50 +08:00
2022-02-16 15:57:28 +08:00
if ( LoadedAssemblies . Values . Any ( t = > Path . GetFileNameWithoutExtension ( t . Assembly . Location ) = = filename ) )
2024-03-23 02:05:58 +08:00
return null ;
2018-04-13 17:19:50 +08:00
try
{
2024-03-23 02:05:58 +08:00
var assembly = Assembly . LoadFrom ( file ) ;
addRuleset ( assembly ) ;
return assembly ;
2018-04-13 17:19:50 +08:00
}
2018-06-02 23:28:29 +08:00
catch ( Exception e )
2018-04-13 17:19:50 +08:00
{
2022-08-18 15:14:38 +08:00
LogFailedLoad ( filename , e ) ;
2018-04-13 17:19:50 +08:00
}
2024-03-23 02:05:58 +08:00
return null ;
2018-04-13 17:19:50 +08:00
}
2019-07-02 23:05:04 +08:00
2019-10-15 15:14:06 +08:00
private void addRuleset ( Assembly assembly )
2019-07-02 23:05:04 +08:00
{
2022-02-16 15:57:28 +08:00
if ( LoadedAssemblies . ContainsKey ( assembly ) )
2019-07-02 23:05:04 +08:00
return ;
2020-08-11 10:09:02 +08:00
// the same assembly may be loaded twice in the same AppDomain (currently a thing in certain Rider versions https://youtrack.jetbrains.com/issue/RIDER-48799).
// as a failsafe, also compare by FullName.
2022-02-16 15:57:28 +08:00
if ( LoadedAssemblies . Any ( a = > a . Key . FullName = = assembly . FullName ) )
2020-08-11 10:09:02 +08:00
return ;
2019-07-02 23:05:04 +08:00
try
{
2022-02-16 15:57:28 +08:00
LoadedAssemblies [ assembly ] = assembly . GetTypes ( ) . First ( t = > t . IsPublic & & t . IsSubclassOf ( typeof ( Ruleset ) ) ) ;
2019-07-02 23:05:04 +08:00
}
catch ( Exception e )
{
2022-12-16 17:16:26 +08:00
LogFailedLoad ( assembly . GetName ( ) . Name ! . Split ( '.' ) . Last ( ) , e ) ;
2019-07-02 23:05:04 +08:00
}
}
2019-10-15 15:14:06 +08:00
public void Dispose ( )
{
Dispose ( true ) ;
GC . SuppressFinalize ( this ) ;
}
2022-11-01 12:57:34 +08:00
protected void Dispose ( bool disposing )
2019-10-15 15:14:06 +08:00
{
2020-04-03 23:32:37 +08:00
AppDomain . CurrentDomain . AssemblyResolve - = resolveRulesetDependencyAssembly ;
2019-10-15 15:14:06 +08:00
}
2021-12-03 16:50:07 +08:00
2022-08-18 15:14:38 +08:00
protected void LogFailedLoad ( string name , Exception exception )
{
2022-11-01 12:57:34 +08:00
Logger . Log ( $"Could not load ruleset \" { name } \ ". Please check for an update from the developer." , level : LogLevel . Error ) ;
2022-08-18 15:14:38 +08:00
Logger . Log ( $"Ruleset load failed: {exception}" ) ;
}
2021-12-03 16:50:07 +08:00
#region Implementation of IRulesetStore
2021-12-14 18:52:54 +08:00
IRulesetInfo ? IRulesetStore . GetRuleset ( int id ) = > GetRuleset ( id ) ;
IRulesetInfo ? IRulesetStore . GetRuleset ( string shortName ) = > GetRuleset ( shortName ) ;
2021-12-03 16:50:07 +08:00
IEnumerable < IRulesetInfo > IRulesetStore . AvailableRulesets = > AvailableRulesets ;
#endregion
2018-04-13 17:19:50 +08:00
}
}