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 ;
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
using osu.Game.Database ;
namespace osu.Game.Rulesets
{
2019-10-15 15:14:06 +08:00
public class RulesetStore : DatabaseBackedStore , IDisposable
2018-04-13 17:19:50 +08:00
{
2019-10-15 15:14:06 +08:00
private const string ruleset_library_prefix = "osu.Game.Rulesets" ;
2018-04-13 17:19:50 +08:00
2019-10-15 15:14:06 +08:00
private readonly Dictionary < Assembly , Type > loadedAssemblies = new Dictionary < Assembly , Type > ( ) ;
2018-04-13 17:19:50 +08:00
2020-04-03 23:32:37 +08:00
private readonly Storage rulesetStorage ;
public RulesetStore ( IDatabaseContextFactory factory , Storage storage = null )
2019-10-15 15:14:06 +08:00
: base ( factory )
{
2020-04-03 23:32:37 +08:00
rulesetStorage = storage ? . GetStorageForDirectory ( "rulesets" ) ;
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.
2021-08-22 13:26:35 +08:00
if ( RuntimeInfo . StartupDirectory ! = null )
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 ;
2020-04-03 23:32:37 +08:00
loadUserRulesets ( ) ;
2019-07-15 14:42:54 +08:00
addMissingRulesets ( ) ;
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>
public RulesetInfo GetRuleset ( int id ) = > AvailableRulesets . FirstOrDefault ( r = > r . ID = = id ) ;
/// <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>
public RulesetInfo GetRuleset ( string shortName ) = > AvailableRulesets . FirstOrDefault ( r = > r . ShortName = = shortName ) ;
/// <summary>
/// All available rulesets.
/// </summary>
2019-07-15 14:42:54 +08:00
public IEnumerable < RulesetInfo > AvailableRulesets { get ; private set ; }
2018-04-13 17:19:50 +08:00
2020-04-03 23:32:37 +08:00
private Assembly resolveRulesetDependencyAssembly ( object sender , ResolveEventArgs args )
{
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.
. Where ( a = > args . Name . Contains ( a . GetName ( ) . Name , StringComparison . Ordinal ) )
// Pick the greatest assembly version.
2020-07-31 15:21:47 +08:00
. OrderByDescending ( a = > a . GetName ( ) . Version )
. FirstOrDefault ( ) ;
2020-07-30 20:10:13 +08:00
if ( domainAssembly ! = null )
return domainAssembly ;
2020-04-03 23:32:37 +08:00
2020-04-05 02:13:46 +08:00
return loadedAssemblies . Keys . FirstOrDefault ( a = > a . FullName = = asm . FullName ) ;
2020-04-03 23:32:37 +08:00
}
2018-04-13 17:19:50 +08:00
2019-07-15 14:42:54 +08:00
private void addMissingRulesets ( )
2018-04-13 17:19:50 +08:00
{
using ( var usage = ContextFactory . GetForWrite ( ) )
{
var context = usage . Context ;
2019-12-18 13:49:09 +08:00
var instances = loadedAssemblies . Values . Select ( r = > ( Ruleset ) Activator . CreateInstance ( r ) ) . ToList ( ) ;
2018-04-13 17:19:50 +08:00
2020-05-05 09:31:11 +08:00
// add all legacy rulesets first to ensure they have exclusive choice of primary key.
2019-12-24 12:48:27 +08:00
foreach ( var r in instances . Where ( r = > r is ILegacyRuleset ) )
2018-04-13 17:19:50 +08:00
{
2019-12-24 15:16:55 +08:00
if ( context . RulesetInfo . SingleOrDefault ( dbRuleset = > dbRuleset . ID = = r . RulesetInfo . ID ) = = null )
2018-04-13 17:19:50 +08:00
context . RulesetInfo . Add ( r . RulesetInfo ) ;
}
context . SaveChanges ( ) ;
2020-10-16 22:40:44 +08:00
var existingRulesets = context . RulesetInfo . ToList ( ) ;
2021-06-18 18:18:57 +08:00
// add any other rulesets which have assemblies present but are not yet in the database.
2019-12-24 12:48:27 +08:00
foreach ( var r in instances . Where ( r = > ! ( r is ILegacyRuleset ) ) )
2019-11-11 19:53:22 +08:00
{
2021-01-07 01:11:47 +08:00
if ( existingRulesets . FirstOrDefault ( ri = > ri . InstantiationInfo . Equals ( r . RulesetInfo . InstantiationInfo , StringComparison . Ordinal ) ) = = null )
2021-06-18 18:18:57 +08:00
{
var existingSameShortName = existingRulesets . FirstOrDefault ( ri = > ri . ShortName = = r . RulesetInfo . ShortName ) ;
if ( existingSameShortName ! = null )
{
// even if a matching InstantiationInfo was not found, there may be an existing ruleset with the same ShortName.
// this generally means the user or ruleset provider has renamed their dll but the underlying ruleset is *likely* the same one.
// in such cases, update the instantiation info of the existing entry to point to the new one.
existingSameShortName . InstantiationInfo = r . RulesetInfo . InstantiationInfo ;
}
else
context . RulesetInfo . Add ( r . RulesetInfo ) ;
}
2019-11-11 19:53:22 +08:00
}
2018-04-13 17:19:50 +08:00
context . SaveChanges ( ) ;
2020-05-05 09:31:11 +08:00
// perform a consistency check
2018-04-13 17:19:50 +08:00
foreach ( var r in context . RulesetInfo )
{
try
{
2021-11-11 16:37:43 +08:00
var resolvedType = Type . GetType ( r . InstantiationInfo )
? ? throw new RulesetLoadException ( @"Type could not be resolved" ) ;
2018-04-13 17:19:50 +08:00
2021-11-11 16:37:43 +08:00
var instanceInfo = ( Activator . CreateInstance ( resolvedType ) as Ruleset ) ? . RulesetInfo
? ? throw new RulesetLoadException ( @"Instantiation failure" ) ;
2018-04-13 17:19:50 +08:00
2021-11-11 16:37:43 +08:00
r . Name = instanceInfo . Name ;
r . ShortName = instanceInfo . ShortName ;
r . InstantiationInfo = instanceInfo . InstantiationInfo ;
r . Available = true ;
2018-04-13 17:19:50 +08:00
}
catch
{
r . Available = false ;
}
}
context . SaveChanges ( ) ;
AvailableRulesets = context . RulesetInfo . Where ( r = > r . Available ) . ToList ( ) ;
}
}
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 ( ) )
{
string rulesetName = ruleset . GetName ( ) . Name ;
if ( ! rulesetName . StartsWith ( ruleset_library_prefix , StringComparison . InvariantCultureIgnoreCase ) | | ruleset . GetName ( ) . Name . Contains ( "Tests" ) )
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
}
2020-04-03 23:32:37 +08:00
private void loadUserRulesets ( )
{
2020-04-07 22:01:47 +08:00
if ( rulesetStorage = = null ) return ;
var rulesets = rulesetStorage . GetFiles ( "." , $"{ruleset_library_prefix}.*.dll" ) ;
2020-04-03 23:32:37 +08:00
2021-10-27 12:04:41 +08:00
foreach ( string ruleset in rulesets . Where ( f = > ! f . Contains ( "Tests" ) ) )
2020-04-07 22:01:47 +08:00
loadRulesetFromFile ( rulesetStorage . GetFullPath ( ruleset ) ) ;
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
{
2021-10-27 12:04:41 +08:00
string [ ] files = Directory . GetFiles ( RuntimeInfo . StartupDirectory , $"{ruleset_library_prefix}.*.dll" ) ;
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
}
}
2019-10-15 15:14:06 +08:00
private void loadRulesetFromFile ( string file )
2018-04-13 17:19:50 +08:00
{
2021-10-27 12:04:41 +08:00
string filename = Path . GetFileNameWithoutExtension ( file ) ;
2018-04-13 17:19:50 +08:00
2021-04-02 06:38:10 +08:00
if ( loadedAssemblies . Values . Any ( t = > Path . GetFileNameWithoutExtension ( t . Assembly . Location ) = = filename ) )
2018-04-13 17:19:50 +08:00
return ;
try
{
2019-07-03 15:51:09 +08:00
addRuleset ( Assembly . LoadFrom ( file ) ) ;
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
{
2019-02-19 11:12:21 +08:00
Logger . Error ( e , $"Failed to load ruleset {filename}" ) ;
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
{
2019-10-15 15:14:06 +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.
if ( loadedAssemblies . Any ( a = > a . Key . FullName = = assembly . FullName ) )
return ;
2019-07-02 23:05:04 +08:00
try
{
2019-10-15 15:14:06 +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 )
{
Logger . Error ( e , $"Failed to add ruleset {assembly}" ) ;
}
}
2019-10-15 15:14:06 +08:00
public void Dispose ( )
{
Dispose ( true ) ;
GC . SuppressFinalize ( this ) ;
}
protected virtual void Dispose ( bool disposing )
{
2020-04-03 23:32:37 +08:00
AppDomain . CurrentDomain . AssemblyResolve - = resolveRulesetDependencyAssembly ;
2019-10-15 15:14:06 +08:00
}
2018-04-13 17:19:50 +08:00
}
}