// Copyright (c) 2021, 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 Foundation import OpenEmuBase public class OECorePlugin: OEPlugin { override public class var pluginExtension: String { "oecoreplugin" } override public class var pluginFolder: String { "Cores" } @objc public class var allPlugins: [OECorePlugin] { // swiftlint:disable:next force_cast return plugins() as! [OECorePlugin] } required init(bundleAtURL bundleURL: URL, name: String?) throws { try super.init(bundleAtURL: bundleURL, name: name) // invalidate global cache Self.cachedRequiredFiles = nil } public static func corePlugin(bundleAtURL bundleURL: URL) -> OECorePlugin? { return try? plugin(bundleAtURL: bundleURL) } public static func corePlugin(bundleIdentifier identifier: String) -> OECorePlugin? { return allPlugins.first(where: { $0.bundleIdentifier.caseInsensitiveCompare(identifier) == .orderedSame }) } public static func corePlugins(forSystemIdentifier identifier: String) -> [OECorePlugin] { return allPlugins.filter { $0.systemIdentifiers.contains(identifier) } } // MARK: - typealias Controller = OEGameCoreController private var _controller: Controller? public var controller: OEGameCoreController! { if _controller == nil, let principalClass = bundle.principalClass { _controller = newPluginController(with: principalClass) } return _controller } private func newPluginController(with bundleClass: AnyClass) -> Controller? { guard let bundleClass = bundleClass as? Controller.Type else { return nil } return bundleClass.init(bundle: bundle) } // MARK: - private static var cachedRequiredFiles: [[String: Any]]? public static var requiredFiles: [[String: Any]] { if cachedRequiredFiles == nil { var files: [[String: Any]] = [] for plugin in allPlugins { files.append(contentsOf: plugin.requiredFiles) } cachedRequiredFiles = files } return cachedRequiredFiles! } public var gameCoreClass: AnyClass? { return controller?.gameCoreClass } public var bundleIdentifier: String { // swiftlint:disable:next force_cast return infoDictionary["CFBundleIdentifier"] as! String } public var systemIdentifiers: [String] { return infoDictionary[OEGameCoreSystemIdentifiersKey] as? [String] ?? [] } public var coreOptions: [String: [String: Any]] { return infoDictionary[OEGameCoreOptionsKey] as? [String: [String: Any]] ?? [:] } public var requiredFiles: [[String: Any]] { var allRequiredFiles: [[String: Any]] = [] for value in coreOptions.values { if let object = value[OEGameCoreRequiredFilesKey] as? [[String: Any]] { allRequiredFiles.append(contentsOf: object) } } return allRequiredFiles } // MARK: - private var isMarkedDeprecatedInInfoPlist: Bool { if infoDictionary[OEGameCoreDeprecatedKey] as? Bool != true { return false } func isValidVersionString(_ string: String) -> Bool { if string.isEmpty { return false } let validCharacters: Set = [".", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9"] return Set(string).isSubset(of: validCharacters) } guard let minMacOSVer = self.infoDictionary[OEGameCoreDeprecatedMinMacOSVersionKey] as? String, isValidVersionString(minMacOSVer) else { return true } let macOSVerComponents = minMacOSVer.components(separatedBy: ".") if macOSVerComponents.count < 2 { return true } let minMacOSVerParsed = OperatingSystemVersion(majorVersion: Int(macOSVerComponents[0])!, minorVersion: Int(macOSVerComponents[1])!, patchVersion: macOSVerComponents.count > 2 ? Int(macOSVerComponents[2])! : 0) if ProcessInfo.processInfo.isOperatingSystemAtLeast(minMacOSVerParsed) { return true } return false } override public var isDeprecated: Bool { if isOutOfSupport { return true } return isMarkedDeprecatedInInfoPlist } override public var isOutOfSupport: Bool { // plugins deprecated 2017-11-04 let bundleFileName = bundle.bundleURL.lastPathComponent let deprecatedPlugins = [ "Dolphin-Core.oecoreplugin", "NeoPop.oecoreplugin", "TwoMbit.oecoreplugin", "VisualBoyAdvance.oecoreplugin", "Yabause.oecoreplugin", ] if deprecatedPlugins.contains(bundleFileName) { return true } // beta-era plugins if let appcastURL = infoDictionary["SUFeedURL"] as? String, appcastURL.contains("openemu.org/update") { return true } // plugins marked as deprecated in the Info.plist keys if isMarkedDeprecatedInInfoPlist, let deadline = infoDictionary[OEGameCoreSupportDeadlineKey] as? Date, Date().compare(deadline) == .orderedDescending { // we are past the support deadline; return true to remove the core prepareForRemoval() return true } // missing value for required key 'CFBundleIdentifier' in Info.plist if infoDictionary["CFBundleIdentifier"] as? String == nil { return true } return false } private func prepareForRemoval() { let replacements = infoDictionary[OEGameCoreSuggestedReplacement] as? [String: String] let defaults = UserDefaults.standard for systemIdentifier in systemIdentifiers { let prefKey = "defaultCore." + systemIdentifier if let currentCore = defaults.string(forKey: prefKey), currentCore == bundleIdentifier { if let replacement = replacements?[systemIdentifier] { defaults.set(replacement, forKey: prefKey) } else { defaults.removeObject(forKey: prefKey) } } } } } public extension OECorePlugin { func requiredFiles(forSystemIdentifier systemIdentifier: String) -> [[String: Any]]? { let options = coreOptions[systemIdentifier] return options?[OEGameCoreRequiredFilesKey] as? [[String: Any]] ?? nil } func requiresFiles(forSystemIdentifier systemIdentifier: String) -> Bool { let options = coreOptions[systemIdentifier] return options?[OEGameCoreRequiresFilesKey] as? Bool ?? false } func supportsCheatCode(forSystemIdentifier systemIdentifier: String) -> Bool { let options = coreOptions[systemIdentifier] return options?[OEGameCoreSupportsCheatCodeKey] as? Bool ?? false } func hasGlitches(forSystemIdentifier systemIdentifier: String) -> Bool { let options = coreOptions[systemIdentifier] return options?[OEGameCoreHasGlitchesKey] as? Bool ?? false } func saveStatesNotSupported(forSystemIdentifier systemIdentifier: String) -> Bool { let options = coreOptions[systemIdentifier] return options?[OEGameCoreSaveStatesNotSupportedKey] as? Bool ?? false } func supportsMultipleDiscs(forSystemIdentifier systemIdentifier: String) -> Bool { let options = coreOptions[systemIdentifier] return options?[OEGameCoreSupportsMultipleDiscsKey] as? Bool ?? false } func supportsRewinding(forSystemIdentifier systemIdentifier: String) -> Bool { let options = coreOptions[systemIdentifier] return options?[OEGameCoreSupportsRewindingKey] as? Bool ?? false } func supportsFileInsertion(forSystemIdentifier systemIdentifier: String) -> Bool { let options = coreOptions[systemIdentifier] return options?[OEGameCoreSupportsFileInsertionKey] as? Bool ?? false } func supportsDisplayModeChange(forSystemIdentifier systemIdentifier: String) -> Bool { let options = coreOptions[systemIdentifier] return options?[OEGameCoreSupportsDisplayModeChangeKey] as? Bool ?? false } func rewindInterval(forSystemIdentifier systemIdentifier: String) -> Int { let options = coreOptions[systemIdentifier] return options?[OEGameCoreRewindIntervalKey] as? Int ?? 0 } func rewindBufferSeconds(forSystemIdentifier systemIdentifier: String) -> Int { let options = coreOptions[systemIdentifier] return options?[OEGameCoreRewindBufferSecondsKey] as? Int ?? 0 } } public extension OECorePlugin.Architecture { // swiftlint:disable:next identifier_name static let x86_64 = "x86_64" static let arm64 = "arm64" } public extension OECorePlugin { typealias Architecture = String var architectures: [Architecture] { var architectures: [Architecture] = [] let executableArchitectures = bundle.executableArchitectures as? [Int] ?? [] if executableArchitectures.contains(NSBundleExecutableArchitectureX86_64) { architectures.append(.x86_64) } if #available(macOS 11.0, *), executableArchitectures.contains(NSBundleExecutableArchitectureARM64) { architectures.append(.arm64) } return architectures } }