2019-01-24 08:43:03 +00: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 09:19:50 +00:00
using System ;
using System.Collections.Generic ;
using System.IO ;
using System.Linq ;
using System.Reflection ;
2020-05-30 23:18:07 +00:00
using osu.Framework ;
2021-05-14 21:45:58 +00:00
using osu.Framework.Extensions.ObjectExtensions ;
2018-06-02 15:28:29 +00:00
using osu.Framework.Logging ;
2020-04-03 15:32:37 +00:00
using osu.Framework.Platform ;
2018-04-13 09:19:50 +00:00
using osu.Game.Database ;
namespace osu.Game.Rulesets
{
2019-10-15 07:14:06 +00:00
public class RulesetStore : DatabaseBackedStore , IDisposable
2018-04-13 09:19:50 +00:00
{
2019-10-15 07:14:06 +00:00
private const string ruleset_library_prefix = "osu.Game.Rulesets" ;
2018-04-13 09:19:50 +00:00
2019-10-15 07:14:06 +00:00
private readonly Dictionary < Assembly , Type > loadedAssemblies = new Dictionary < Assembly , Type > ( ) ;
2018-04-13 09:19:50 +00:00
2020-04-03 15:32:37 +00:00
private readonly Storage rulesetStorage ;
public RulesetStore ( IDatabaseContextFactory factory , Storage storage = null )
2019-10-15 07:14:06 +00:00
: base ( factory )
{
2020-04-03 15:32:37 +00:00
rulesetStorage = storage ? . GetStorageForDirectory ( "rulesets" ) ;
2019-07-03 09:36:04 +00:00
// On android in release configuration assemblies are loaded from the apk directly into memory.
2019-07-03 09:41:01 +00:00
// We cannot read assemblies from cwd, so should check loaded assemblies instead.
2019-07-03 09:42:10 +00:00
loadFromAppDomain ( ) ;
2021-08-22 05:26:35 +00:00
2021-08-22 09:40:41 +00: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 05:26:35 +00:00
if ( RuntimeInfo . StartupDirectory ! = null )
loadFromDisk ( ) ;
2020-04-19 14:29:32 +00:00
2020-04-19 13:25:21 +00: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 10:20:54 +00:00
AppDomain . CurrentDomain . AssemblyResolve + = resolveRulesetDependencyAssembly ;
2020-04-03 15:32:37 +00:00
loadUserRulesets ( ) ;
2019-07-15 06:42:54 +00:00
addMissingRulesets ( ) ;
2018-04-13 09:19:50 +00: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 06:42:54 +00:00
public IEnumerable < RulesetInfo > AvailableRulesets { get ; private set ; }
2018-04-13 09:19:50 +00:00
2020-04-03 15:32:37 +00:00
private Assembly resolveRulesetDependencyAssembly ( object sender , ResolveEventArgs args )
{
var asm = new AssemblyName ( args . Name ) ;
2020-04-07 10:20:54 +00:00
// the requesting assembly may be located out of the executable's base directory, thus requiring manual resolving of its dependencies.
2020-04-20 11:56:15 +00: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 12:10:13 +00: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 07:21:47 +00:00
. OrderByDescending ( a = > a . GetName ( ) . Version )
. FirstOrDefault ( ) ;
2020-07-30 12:10:13 +00:00
if ( domainAssembly ! = null )
return domainAssembly ;
2020-04-03 15:32:37 +00:00
2020-04-04 18:13:46 +00:00
return loadedAssemblies . Keys . FirstOrDefault ( a = > a . FullName = = asm . FullName ) ;
2020-04-03 15:32:37 +00:00
}
2018-04-13 09:19:50 +00:00
2019-07-15 06:42:54 +00:00
private void addMissingRulesets ( )
2018-04-13 09:19:50 +00:00
{
using ( var usage = ContextFactory . GetForWrite ( ) )
{
var context = usage . Context ;
2019-12-18 05:49:09 +00:00
var instances = loadedAssemblies . Values . Select ( r = > ( Ruleset ) Activator . CreateInstance ( r ) ) . ToList ( ) ;
2018-04-13 09:19:50 +00:00
2020-05-05 01:31:11 +00:00
// add all legacy rulesets first to ensure they have exclusive choice of primary key.
2019-12-24 04:48:27 +00:00
foreach ( var r in instances . Where ( r = > r is ILegacyRuleset ) )
2018-04-13 09:19:50 +00:00
{
2019-12-24 07:16:55 +00:00
if ( context . RulesetInfo . SingleOrDefault ( dbRuleset = > dbRuleset . ID = = r . RulesetInfo . ID ) = = null )
2018-04-13 09:19:50 +00:00
context . RulesetInfo . Add ( r . RulesetInfo ) ;
}
context . SaveChanges ( ) ;
2020-10-16 14:40:44 +00:00
var existingRulesets = context . RulesetInfo . ToList ( ) ;
2021-06-18 10:18:57 +00:00
// add any other rulesets which have assemblies present but are not yet in the database.
2019-12-24 04:48:27 +00:00
foreach ( var r in instances . Where ( r = > ! ( r is ILegacyRuleset ) ) )
2019-11-11 11:53:22 +00:00
{
2021-01-06 17:11:47 +00:00
if ( existingRulesets . FirstOrDefault ( ri = > ri . InstantiationInfo . Equals ( r . RulesetInfo . InstantiationInfo , StringComparison . Ordinal ) ) = = null )
2021-06-18 10:18:57 +00: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 11:53:22 +00:00
}
2018-04-13 09:19:50 +00:00
context . SaveChanges ( ) ;
2020-05-05 01:31:11 +00:00
// perform a consistency check
2018-04-13 09:19:50 +00:00
foreach ( var r in context . RulesetInfo )
{
try
{
2021-05-14 21:45:58 +00:00
var instanceInfo = ( ( Ruleset ) Activator . CreateInstance ( Type . GetType ( r . InstantiationInfo ) . AsNonNull ( ) ) ) . RulesetInfo ;
2018-04-13 09:19:50 +00:00
2018-06-28 07:36:42 +00:00
r . Name = instanceInfo . Name ;
r . ShortName = instanceInfo . ShortName ;
r . InstantiationInfo = instanceInfo . InstantiationInfo ;
2018-04-13 09:19:50 +00:00
r . Available = true ;
}
catch
{
r . Available = false ;
}
}
context . SaveChanges ( ) ;
AvailableRulesets = context . RulesetInfo . Where ( r = > r . Available ) . ToList ( ) ;
}
}
2019-10-15 07:14:06 +00:00
private void loadFromAppDomain ( )
2019-07-02 15:05:04 +00:00
{
2019-07-02 15:25:12 +00: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 15:05:04 +00:00
addRuleset ( ruleset ) ;
2019-07-02 15:25:12 +00:00
}
2019-07-02 15:05:04 +00:00
}
2020-04-03 15:32:37 +00:00
private void loadUserRulesets ( )
{
2020-04-07 14:01:47 +00:00
if ( rulesetStorage = = null ) return ;
var rulesets = rulesetStorage . GetFiles ( "." , $"{ruleset_library_prefix}.*.dll" ) ;
2020-04-03 15:32:37 +00:00
2020-04-07 10:20:54 +00:00
foreach ( var ruleset in rulesets . Where ( f = > ! f . Contains ( "Tests" ) ) )
2020-04-07 14:01:47 +00:00
loadRulesetFromFile ( rulesetStorage . GetFullPath ( ruleset ) ) ;
2020-04-03 15:32:37 +00:00
}
2019-10-15 07:14:06 +00:00
private void loadFromDisk ( )
2019-07-03 09:42:10 +00:00
{
try
{
2020-06-02 05:54:55 +00:00
var files = Directory . GetFiles ( RuntimeInfo . StartupDirectory , $"{ruleset_library_prefix}.*.dll" ) ;
2019-07-03 09:42:10 +00:00
foreach ( string file in files . Where ( f = > ! Path . GetFileName ( f ) . Contains ( "Tests" ) ) )
loadRulesetFromFile ( file ) ;
}
2019-10-01 06:41:01 +00:00
catch ( Exception e )
2019-07-03 09:42:10 +00:00
{
2020-05-30 23:18:07 +00:00
Logger . Error ( e , $"Could not load rulesets from directory {RuntimeInfo.StartupDirectory}" ) ;
2019-07-03 09:42:10 +00:00
}
}
2019-10-15 07:14:06 +00:00
private void loadRulesetFromFile ( string file )
2018-04-13 09:19:50 +00:00
{
var filename = Path . GetFileNameWithoutExtension ( file ) ;
2021-04-01 22:38:10 +00:00
if ( loadedAssemblies . Values . Any ( t = > Path . GetFileNameWithoutExtension ( t . Assembly . Location ) = = filename ) )
2018-04-13 09:19:50 +00:00
return ;
try
{
2019-07-03 07:51:09 +00:00
addRuleset ( Assembly . LoadFrom ( file ) ) ;
2018-04-13 09:19:50 +00:00
}
2018-06-02 15:28:29 +00:00
catch ( Exception e )
2018-04-13 09:19:50 +00:00
{
2019-02-19 03:12:21 +00:00
Logger . Error ( e , $"Failed to load ruleset {filename}" ) ;
2018-04-13 09:19:50 +00:00
}
}
2019-07-02 15:05:04 +00:00
2019-10-15 07:14:06 +00:00
private void addRuleset ( Assembly assembly )
2019-07-02 15:05:04 +00:00
{
2019-10-15 07:14:06 +00:00
if ( loadedAssemblies . ContainsKey ( assembly ) )
2019-07-02 15:05:04 +00:00
return ;
2020-08-11 02:09:02 +00: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 15:05:04 +00:00
try
{
2019-10-15 07:14:06 +00:00
loadedAssemblies [ assembly ] = assembly . GetTypes ( ) . First ( t = > t . IsPublic & & t . IsSubclassOf ( typeof ( Ruleset ) ) ) ;
2019-07-02 15:05:04 +00:00
}
catch ( Exception e )
{
Logger . Error ( e , $"Failed to add ruleset {assembly}" ) ;
}
}
2019-10-15 07:14:06 +00:00
public void Dispose ( )
{
Dispose ( true ) ;
GC . SuppressFinalize ( this ) ;
}
protected virtual void Dispose ( bool disposing )
{
2020-04-03 15:32:37 +00:00
AppDomain . CurrentDomain . AssemblyResolve - = resolveRulesetDependencyAssembly ;
2019-10-15 07:14:06 +00:00
}
2018-04-13 09:19:50 +00:00
}
}