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 ;
2021-11-23 12:00:33 +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
using osu.Game.Database ;
2021-11-23 12:00:33 +08:00
#nullable enable
2018-04-13 17:19:50 +08:00
namespace osu.Game.Rulesets
{
2021-12-14 18:52:54 +08:00
public class RulesetStore : IDisposable , IRulesetStore
2018-04-13 17:19:50 +08:00
{
2022-01-25 12:09:47 +08:00
private readonly RealmAccess realmAccess ;
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
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
2021-11-23 12:00:33 +08:00
/// <summary>
/// All available rulesets.
/// </summary>
public IEnumerable < RulesetInfo > AvailableRulesets = > availableRulesets ;
private readonly List < RulesetInfo > availableRulesets = new List < RulesetInfo > ( ) ;
2020-04-03 23:32:37 +08:00
2022-01-24 18:59:58 +08:00
public RulesetStore ( RealmAccess realm , Storage ? storage = null )
2019-10-15 15:14:06 +08:00
{
2022-01-25 12:09:47 +08:00
realmAccess = realm ;
2020-04-03 23:32:37 +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.
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 ;
2021-11-23 12:00:33 +08:00
var rulesetStorage = storage ? . GetStorageForDirectory ( @"rulesets" ) ;
if ( rulesetStorage ! = null )
loadUserRulesets ( rulesetStorage ) ;
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>
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 ) ;
} )
2020-07-30 20:10:13 +08:00
// 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
{
2022-01-25 12:09:47 +08:00
realmAccess . Write ( realm = >
2018-04-13 17:19:50 +08:00
{
2022-01-21 16:08:20 +08:00
var rulesets = realm . All < RulesetInfo > ( ) ;
2018-04-13 17:19:50 +08:00
2022-01-21 16:08:20 +08:00
List < Ruleset > instances = loadedAssemblies . Values
. Select ( r = > Activator . CreateInstance ( r ) as Ruleset )
. Where ( r = > r ! = null )
. Select ( r = > r . AsNonNull ( ) )
. ToList ( ) ;
2020-10-16 22:40:44 +08:00
2022-01-21 16:08:20 +08:00
// add all legacy rulesets first to ensure they have exclusive choice of primary key.
foreach ( var r in instances . Where ( r = > r is ILegacyRuleset ) )
{
if ( realm . All < RulesetInfo > ( ) . FirstOrDefault ( rr = > rr . OnlineID = = r . RulesetInfo . OnlineID ) = = null )
realm . Add ( new RulesetInfo ( r . RulesetInfo . ShortName , r . RulesetInfo . Name , r . RulesetInfo . InstantiationInfo , r . RulesetInfo . OnlineID ) ) ;
}
2021-06-18 18:18:57 +08:00
2022-01-21 16:08:20 +08:00
// add any other rulesets which have assemblies present but are not yet in the database.
foreach ( var r in instances . Where ( r = > ! ( r is ILegacyRuleset ) ) )
{
if ( rulesets . FirstOrDefault ( ri = > ri . InstantiationInfo . Equals ( r . RulesetInfo . InstantiationInfo , StringComparison . Ordinal ) ) = = null )
2021-11-23 12:00:33 +08:00
{
2022-01-21 16:08:20 +08:00
var existingSameShortName = rulesets . FirstOrDefault ( ri = > ri . ShortName = = r . RulesetInfo . ShortName ) ;
if ( existingSameShortName ! = null )
2021-06-18 18:18:57 +08:00
{
2022-01-21 16:08:20 +08:00
// 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 ;
2021-06-18 18:18:57 +08:00
}
2022-01-21 16:08:20 +08:00
else
realm . Add ( new RulesetInfo ( r . RulesetInfo . ShortName , r . RulesetInfo . Name , r . RulesetInfo . InstantiationInfo , r . RulesetInfo . OnlineID ) ) ;
2021-06-18 18:18:57 +08:00
}
2022-01-21 16:08:20 +08:00
}
2018-04-13 17:19:50 +08:00
2022-01-21 16:08:20 +08:00
List < RulesetInfo > detachedRulesets = new List < RulesetInfo > ( ) ;
2018-04-13 17:19:50 +08:00
2022-01-21 16:08:20 +08:00
// perform a consistency check and detach final rulesets from realm for cross-thread runtime usage.
foreach ( var r in rulesets . OrderBy ( r = > r . OnlineID ) )
{
try
2018-04-13 17:19:50 +08:00
{
2022-01-21 16:08:20 +08:00
var resolvedType = Type . GetType ( r . InstantiationInfo )
? ? throw new RulesetLoadException ( @"Type could not be resolved" ) ;
2018-04-13 17:19:50 +08:00
2022-01-21 16:08:20 +08:00
var instanceInfo = ( Activator . CreateInstance ( resolvedType ) as Ruleset ) ? . RulesetInfo
? ? throw new RulesetLoadException ( @"Instantiation failure" ) ;
2018-04-13 17:19:50 +08:00
2022-02-02 13:51:55 +08:00
// If a ruleset isn't up-to-date with the API, it could cause a crash at an arbitrary point of execution.
// To eagerly handle cases of missing implementations, enumerate all types here and mark as non-available on throw.
resolvedType . Assembly . GetTypes ( ) ;
2022-01-21 16:08:20 +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
2022-01-21 16:08:20 +08:00
detachedRulesets . Add ( r . Clone ( ) ) ;
}
catch ( Exception ex )
{
r . Available = false ;
Logger . Log ( $"Could not load ruleset {r}: {ex.Message}" ) ;
2021-11-23 12:00:33 +08:00
}
2022-01-21 16:08:20 +08:00
}
2018-04-13 17:19:50 +08:00
2022-01-27 14:59:20 +08:00
availableRulesets . AddRange ( detachedRulesets . OrderBy ( r = > r ) ) ;
2022-01-21 16:08:20 +08:00
} ) ;
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" ) ) )
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-11-23 12:00:33 +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-11-23 12:00:33 +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
}
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
}
}