mirror of
https://github.com/OpenEmu/OpenEmuKit.git
synced 2025-11-01 11:08:14 +00:00
450 lines
15 KiB
Objective-C
450 lines
15 KiB
Objective-C
/*
|
|
Copyright (c) 2009, OpenEmu Team
|
|
|
|
Redistribution and use in source and binary forms, with or without
|
|
modification, are permitted provided that the following conditions are met:
|
|
* Redistributions of source code must retain the above copyright
|
|
notice, this list of conditions and the following disclaimer.
|
|
* Redistributions in binary form must reproduce the above copyright
|
|
notice, this list of conditions and the following disclaimer in the
|
|
documentation and/or other materials provided with the distribution.
|
|
* Neither the name of the OpenEmu Team nor the
|
|
names of its contributors may be used to endorse or promote products
|
|
derived from this software without specific prior written permission.
|
|
|
|
THIS SOFTWARE IS PROVIDED BY OpenEmu Team ''AS IS'' AND ANY
|
|
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
DISCLAIMED. IN NO EVENT SHALL OpenEmu Team BE LIABLE FOR ANY
|
|
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
|
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
#import "OEPlugin.h"
|
|
#import <objc/runtime.h>
|
|
#import "OELogging.h"
|
|
|
|
@implementation NSObject (OEPlugin)
|
|
+ (BOOL)isPluginClass
|
|
{
|
|
return [self isSubclassOfClass:[OEPlugin class]];
|
|
}
|
|
@end
|
|
|
|
NSInteger OE_compare(OEPlugin *obj1, OEPlugin *obj2, void *ctx);
|
|
|
|
@implementation OEPlugin
|
|
@synthesize controller = _controller;
|
|
|
|
static NSMutableDictionary *_allPlugins = nil;
|
|
static NSMutableDictionary *_pluginFolders = nil;
|
|
static NSMutableDictionary *_needsReload = nil;
|
|
static NSMutableSet *_allPluginClasses = nil;
|
|
static NSMutableDictionary *_pluginsForPathsByTypes = nil;
|
|
static NSMutableDictionary *_pluginsForNamesByTypes = nil;
|
|
|
|
+ (void)initialize
|
|
{
|
|
if(self == [OEPlugin class])
|
|
{
|
|
_allPlugins = [[NSMutableDictionary alloc] init];
|
|
_pluginFolders = [[NSMutableDictionary alloc] init];
|
|
_needsReload = [[NSMutableDictionary alloc] init];
|
|
_allPluginClasses = [[NSMutableSet alloc] init];
|
|
_pluginsForPathsByTypes = [[NSMutableDictionary alloc] init];
|
|
_pluginsForNamesByTypes = [[NSMutableDictionary alloc] init];
|
|
}
|
|
else [self registerPluginClass:self];
|
|
}
|
|
|
|
+ (NSSet *)pluginClasses;
|
|
{
|
|
return [_allPluginClasses copy];
|
|
}
|
|
|
|
+ (void)registerPluginClass;
|
|
{
|
|
[_allPluginClasses addObject:self];
|
|
|
|
[self pluginsForType:self];
|
|
}
|
|
|
|
+ (void)registerPluginClass:(Class)pluginClass;
|
|
{
|
|
NSAssert1([pluginClass isPluginClass], @"Class %@ is not a subclass of OEPlugin.", pluginClass);
|
|
|
|
[pluginClass registerPluginClass];
|
|
}
|
|
|
|
+ (NSString *)pluginType
|
|
{
|
|
return NSStringFromClass(self);
|
|
}
|
|
|
|
+ (NSString *)pluginFolder
|
|
{
|
|
NSString *ret = [_pluginFolders objectForKey:self];
|
|
if(ret == nil)
|
|
{
|
|
ret = [self pluginType];
|
|
NSRange c = [ret rangeOfCharacterFromSet:[NSCharacterSet lowercaseLetterCharacterSet]];
|
|
if(c.location != NSNotFound && c.location > 0)
|
|
{
|
|
ret = [ret substringFromIndex:c.location - 1];
|
|
if([ret hasSuffix:@"Plugin"])
|
|
ret = [ret substringToIndex:[ret length] - 6];
|
|
}
|
|
ret = [ret stringByAppendingString:@"s"];
|
|
[_pluginFolders setObject:ret forKey:(id<NSCopying>)self];
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
+ (NSString *)pluginExtension
|
|
{
|
|
return [[self pluginType] lowercaseString];
|
|
}
|
|
|
|
+ (Class)typeForExtension:(NSString *)anExtension
|
|
{
|
|
for(Class cls in _allPlugins)
|
|
if([[cls pluginExtension] isEqualToString:anExtension])
|
|
return cls;
|
|
|
|
NSInteger len = [anExtension length] - 8;
|
|
if(len > 0) return NSClassFromString([NSString stringWithFormat:@"OE%@Plugin",
|
|
[[anExtension substringWithRange:NSMakeRange(2, len)]
|
|
capitalizedString]]);
|
|
return Nil;
|
|
}
|
|
|
|
+ (BOOL)isPluginClass
|
|
{
|
|
return YES;
|
|
}
|
|
|
|
+ (NSMutableDictionary *)OE_pluginsForNamesOfType:(Class)cls createIfNeeded:(BOOL)create
|
|
{
|
|
NSMutableDictionary *plugins = [_pluginsForNamesByTypes objectForKey:cls];
|
|
if(plugins == nil && create)
|
|
{
|
|
plugins = [NSMutableDictionary dictionary];
|
|
_pluginsForNamesByTypes[(id)cls] = plugins;
|
|
}
|
|
return plugins;
|
|
}
|
|
|
|
+ (NSMutableDictionary *)OE_pluginsForPathsOfType:(Class)cls createIfNeeded:(BOOL)create
|
|
{
|
|
NSMutableDictionary *plugins = [_pluginsForPathsByTypes objectForKey:cls];
|
|
if(plugins == nil && create)
|
|
{
|
|
plugins = [NSMutableDictionary dictionary];
|
|
_pluginsForPathsByTypes[(id)cls] = plugins;
|
|
}
|
|
return plugins;
|
|
}
|
|
|
|
// When an instance is assigned as objectValue to a NSCell, the NSCell creates a copy.
|
|
// Therefore we have to implement the NSCopying protocol
|
|
// No need to make an actual copy, we can consider each OECorePlugin instances like a singleton for their bundle
|
|
- (id)copyWithZone:(NSZone *)zone
|
|
{
|
|
return self;
|
|
}
|
|
|
|
- (void)OE_setupWithBundleAtPath:(NSString *)aPath;
|
|
{
|
|
NSBundle *bundle = [NSBundle bundleWithPath:aPath];
|
|
if(bundle == nil) return;
|
|
|
|
_bundle = bundle;
|
|
_path = [_bundle bundlePath];
|
|
_infoDictionary = [_bundle infoDictionary];
|
|
_version = [_infoDictionary objectForKey:@"CFBundleVersion"];
|
|
_displayName = [_infoDictionary objectForKey:@"CFBundleName"] ? : [_infoDictionary objectForKey:@"CFBundleExecutable"];
|
|
}
|
|
|
|
- (id)initWithFileAtPath:(NSString *)aPath name:(NSString *)aName error:(NSError *__autoreleasing *)outError
|
|
{
|
|
if(aPath == nil && aName == nil) {
|
|
if (outError) *outError = nil;
|
|
return nil;
|
|
}
|
|
|
|
id existing = ( [[self class] OE_pluginsForNamesOfType:[self class] createIfNeeded:NO][aName]
|
|
? : [[self class] OE_pluginsForPathsOfType:[self class] createIfNeeded:NO][aPath]);
|
|
if(existing != nil) return existing;
|
|
|
|
if((self = [super init]))
|
|
{
|
|
if(aPath != nil && ![[aPath pathExtension] isEqualToString:[[self class] pluginExtension]]) {
|
|
if (outError) *outError = [NSError errorWithDomain:OEGameCoreErrorDomain code:OEGameCorePluginInvalidError userInfo:nil];
|
|
return nil;
|
|
}
|
|
|
|
_path = [aPath copy];
|
|
_name = [aName copy] ? : [[_path lastPathComponent] stringByDeletingPathExtension];
|
|
|
|
if(_path != nil)
|
|
{
|
|
[self OE_setupWithBundleAtPath:aPath];
|
|
if (self.outOfSupport) {
|
|
/* plugin must be removed */
|
|
os_log(OE_LOG_DEFAULT, "Removing out-of-support plugin %{public}@", _path);
|
|
NSFileManager *fm = [NSFileManager defaultManager];
|
|
NSError *error;
|
|
if (![fm removeItemAtPath:_path error:&error]) {
|
|
os_log_error(OE_LOG_DEFAULT, "Error when removing out-of-support plugin: %{public}@", error);
|
|
}
|
|
if (outError) *outError = [NSError errorWithDomain:OEGameCoreErrorDomain code:OEGameCorePluginOutOfSupportError userInfo:nil];
|
|
return nil;
|
|
}
|
|
[[self class] OE_pluginsForPathsOfType:[self class] createIfNeeded:YES][_path] = self;
|
|
}
|
|
|
|
[[self class] OE_pluginsForNamesOfType:[self class] createIfNeeded:YES][_name] = self;
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (id)initWithFileAtPath:(NSString *)aPath name:(NSString *)aName
|
|
{
|
|
return [self initWithFileAtPath:aPath name:aName error:NULL];
|
|
}
|
|
|
|
- (id)initWithBundle:(NSBundle *)aBundle
|
|
{
|
|
return [self initWithFileAtPath:[aBundle bundlePath] name:nil];
|
|
}
|
|
|
|
- (void)dealloc
|
|
{
|
|
[_bundle unload];
|
|
}
|
|
|
|
- (id)controller
|
|
{
|
|
if (_controller == nil)
|
|
{
|
|
Class principalClass = [[self bundle] principalClass];
|
|
_controller = [self newPluginControllerWithClass:principalClass];
|
|
}
|
|
return _controller;
|
|
}
|
|
|
|
- (id)newPluginControllerWithClass:(Class)bundleClass
|
|
{
|
|
return [[bundleClass alloc] init];
|
|
}
|
|
|
|
static NSString *OE_pluginPathForNameType(NSString *aName, Class aType)
|
|
{
|
|
NSString *folderName = [aType pluginFolder];
|
|
NSString *extension = [aType pluginExtension];
|
|
|
|
NSString *openEmuSearchPath = [@"OpenEmu" stringByAppendingPathComponent:
|
|
[folderName stringByAppendingPathComponent:
|
|
[aName stringByAppendingPathExtension:extension]]];
|
|
|
|
NSString *ret = nil;
|
|
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES);
|
|
|
|
NSFileManager *manager = [NSFileManager defaultManager];
|
|
for(NSString *path in paths)
|
|
{
|
|
NSString *tested = [path stringByAppendingPathComponent:openEmuSearchPath];
|
|
if([manager fileExistsAtPath:tested])
|
|
{
|
|
ret = tested;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(ret == nil) ret = [[NSBundle mainBundle] pathForResource:aName ofType:extension inDirectory:folderName];
|
|
return ret;
|
|
}
|
|
|
|
- (NSString *)details
|
|
{
|
|
return _bundle != nil ? [NSString stringWithFormat:@"Version %@", [self version]] : nil;
|
|
}
|
|
|
|
- (NSString *)description
|
|
{
|
|
return ( _bundle != nil
|
|
? [NSString stringWithFormat:@"Type: %@, Bundle: %@, Version: %@", [[self class] pluginType], _displayName, _version]
|
|
: [NSString stringWithFormat:@"Type: %@, Path: %@", [[self class] pluginType], _path]);
|
|
}
|
|
|
|
- (BOOL)isDeprecated
|
|
{
|
|
return self.outOfSupport;
|
|
}
|
|
|
|
- (BOOL)isOutOfSupport
|
|
{
|
|
return NO;
|
|
}
|
|
|
|
NSInteger OE_compare(OEPlugin *obj1, OEPlugin *obj2, void *ctx)
|
|
{
|
|
return [[obj1 displayName] caseInsensitiveCompare:[obj2 displayName]];
|
|
}
|
|
|
|
+ (NSArray *)pluginsForType:(Class)aType
|
|
{
|
|
NSArray *ret = nil;
|
|
if([aType isPluginClass] && [_allPluginClasses containsObject:aType])
|
|
{
|
|
NSMutableDictionary *plugins = [_allPlugins objectForKey:aType];
|
|
if(plugins == nil)
|
|
{
|
|
NSString *folder = [aType pluginFolder];
|
|
NSString *extension = [aType pluginExtension];
|
|
|
|
if(extension == nil) return nil;
|
|
|
|
NSString *openEmuSearchPath = [@"OpenEmu" stringByAppendingPathComponent:folder];
|
|
|
|
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES);
|
|
NSFileManager *manager = [NSFileManager defaultManager];
|
|
|
|
for(NSString *path in paths)
|
|
{
|
|
NSString *subpath = [path stringByAppendingPathComponent:openEmuSearchPath];
|
|
NSArray *subpaths = [manager contentsOfDirectoryAtPath:subpath error:NULL];
|
|
for(NSString *bundlePath in subpaths)
|
|
if([extension isEqualToString:[bundlePath pathExtension]])
|
|
// If a plugin fails to load, another plugin with the same name in deeper paths can take its spot
|
|
// Typical case: an old-style plugin in ~/Library should fail to load,
|
|
// and its new-style counter-part in /Library will take its place and load properly
|
|
[self pluginWithFileAtPath:[subpath stringByAppendingPathComponent:bundlePath] type:aType forceReload:YES];
|
|
}
|
|
|
|
NSString *pluginFolderPath = [[[NSBundle mainBundle] builtInPlugInsPath] stringByAppendingPathComponent:folder];
|
|
paths = [manager contentsOfDirectoryAtPath:pluginFolderPath error:NULL];
|
|
for(NSString *path in paths)
|
|
if([extension isEqualToString:[path pathExtension]])
|
|
[self pluginWithFileAtPath:[pluginFolderPath stringByAppendingPathComponent:path] type:aType];
|
|
|
|
plugins = [_allPlugins objectForKey:aType];
|
|
}
|
|
|
|
NSMutableSet *set = [NSMutableSet setWithArray:[plugins allValues]];
|
|
[set removeObject:[NSNull null]];
|
|
ret = [[set allObjects] sortedArrayUsingFunction:OE_compare context:nil];
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
+ (NSArray *)allPlugins
|
|
{
|
|
NSArray *ret = nil;
|
|
if(self == [OEPlugin class])
|
|
{
|
|
NSMutableArray *temp = [NSMutableArray array];
|
|
for(Class key in _allPlugins)
|
|
[temp addObjectsFromArray:[self pluginsForType:key]];
|
|
|
|
ret = [temp copy];
|
|
}
|
|
else ret = [self pluginsForType:self];
|
|
|
|
return ret;
|
|
}
|
|
|
|
+ (NSArray *)allPluginNames;
|
|
{
|
|
return [[self allPlugins] valueForKey:@"name"];
|
|
}
|
|
|
|
+ (instancetype)pluginWithBundle:(NSBundle *)aBundle type:(Class)aType forceReload:(BOOL)reload
|
|
{
|
|
return [self pluginWithFileAtPath:[aBundle bundlePath] type:aType forceReload:reload];
|
|
}
|
|
|
|
+ (instancetype)pluginWithFileAtPath:(NSString *)filePath type:(Class)aType forceReload:(BOOL)reload
|
|
{
|
|
return [self pluginWithFileAtPath:filePath type:aType forceReload:reload error:NULL];
|
|
}
|
|
|
|
+ (instancetype)pluginWithFileAtPath:(NSString *)filePath type:(Class)aType forceReload:(BOOL)reload error:(NSError *__autoreleasing *)outError
|
|
{
|
|
if(filePath == nil || ![aType isPluginClass]) {
|
|
if (outError) *outError = nil;
|
|
return nil;
|
|
}
|
|
|
|
NSMutableDictionary *plugins = [_allPlugins objectForKey:aType];
|
|
if(plugins == nil)
|
|
{
|
|
plugins = [NSMutableDictionary dictionary];
|
|
[_allPlugins setObject:plugins forKey:(id<NSCopying>)aType];
|
|
}
|
|
|
|
NSString *pluginName = [[filePath lastPathComponent] stringByDeletingPathExtension];
|
|
id ret = [plugins objectForKey:pluginName];
|
|
|
|
if(reload)
|
|
{
|
|
// Will override a previous failed attempt at loading a plugin
|
|
if(ret == [NSNull null]) ret = nil;
|
|
// A plugin was already successfully loaded
|
|
else if(ret != nil) {
|
|
if (outError) *outError = [NSError errorWithDomain:OEGameCoreErrorDomain code:OEGameCorePluginAlreadyLoadedError userInfo:nil];
|
|
return nil;
|
|
}
|
|
}
|
|
|
|
// No plugin with such name, attempt to actually load the file at the given path
|
|
if(ret == nil)
|
|
{
|
|
[OEPlugin willChangeValueForKey:@"allPlugins"];
|
|
[aType willChangeValueForKey:@"allPlugins"];
|
|
|
|
ret = [[aType alloc] initWithFileAtPath:filePath name:pluginName error:outError];
|
|
|
|
// If ret is still nil at this point, it means the plugin can't be loaded (old-style version for example)
|
|
if(ret == nil) ret = [NSNull null];
|
|
|
|
[plugins setObject:ret forKey:pluginName];
|
|
[aType didChangeValueForKey:@"allPlugins"];
|
|
[OEPlugin didChangeValueForKey:@"allPlugins"];
|
|
}
|
|
|
|
if(ret == [NSNull null]) ret = nil;
|
|
|
|
return ret;
|
|
}
|
|
|
|
+ (instancetype)pluginWithFileAtPath:(NSString *)aPath type:(Class)aType
|
|
{
|
|
return [self pluginWithFileAtPath:aPath type:aType forceReload:NO];
|
|
}
|
|
|
|
+ (instancetype)pluginWithName:(NSString *)aName
|
|
{
|
|
if(self == [OEPlugin class]) return nil;
|
|
|
|
return [self pluginWithName:aName type:self];
|
|
}
|
|
|
|
+ (instancetype)pluginWithName:(NSString *)aName type:(Class)aType
|
|
{
|
|
return ( [self OE_pluginsForNamesOfType:aType createIfNeeded:NO][aName]
|
|
? : [self pluginWithFileAtPath:OE_pluginPathForNameType(aName, aType) type:aType]);
|
|
}
|
|
|
|
- (NSArray *)availablePreferenceViewControllerKeys
|
|
{
|
|
return _bundle != nil ? [[self controller] availablePreferenceViewControllerKeys] : nil;
|
|
}
|
|
|
|
@end
|