// Copyright 1998-2014 Epic Games, Inc. All Rights Reserved. #include "ProjectsPrivatePCH.h" DEFINE_LOG_CATEGORY_STATIC( LogPluginManager, Log, All ); #define LOCTEXT_NAMESPACE "PluginManager" namespace PluginSystemDefs { /** Latest supported version for plugin descriptor files. We can still usually load older versions, but not newer version. NOTE: This constant exists in UnrealBuildTool code as well. */ static const int32 LatestPluginDescriptorFileVersion = 1; // IMPORTANT: Remember to also update Plugins.cs (and Plugin system documentation) when this changes! /** File extension of plugin descriptor files. NOTE: This constant exists in UnrealBuildTool code as well. */ static const TCHAR PluginDescriptorFileExtension[] = TEXT( ".uplugin" ); /** Relative path to the plugin's 128x128 icon resource file */ static const FString RelativeIcon128FilePath( TEXT( "Resources/Icon128.png" ) ); } FPluginManager::FPluginManager() : bEarliestPhaseProcessed(false) , bRestartRequired(false) { DiscoverAllPlugins(); } FPluginManager::~FPluginManager() { // NOTE: All plugins and modules should be cleaned up or abandoned by this point // @todo plugin: Really, we should "reboot" module manager's unloading code so that it remembers at which startup phase // modules were loaded in, so that we can shut groups of modules down (in reverse-load order) at the various counterpart // shutdown phases. This will fix issues where modules that are loaded after game modules are shutdown AFTER many engine // systems are already killed (like GConfig.) Currently the only workaround is to listen to global exit events, or to // explicitly unload your module somewhere. We should be able to handle most cases automatically though! } void FPluginManager::DiscoverAllPlugins() { struct Local { private: /** * Recursively searches for plugins and generates a list of plugin descriptors * * @param PluginsDirectory Directory we're currently searching * @param LoadedFrom Where we're loading these plugins from (game, engine, etc) * @param Plugins The array to be filled in with new plugins including descriptors */ static void FindPluginsRecursively( const FString& PluginsDirectory, const FPluginInfo::ELoadedFrom::Type LoadedFrom, TArray< TSharedRef >& Plugins ) { // NOTE: The logic in this function generally matches that of the C# code for FindPluginsRecursively // in UnrealBuildTool. These routines should be kept in sync. // This directory scanning needs to be fast because it will happen every time at startup! We don't // want to blindly recurse down every subdirectory, because plugin content and code directories could // contain a lot of files in total! // Each sub-directory is possibly a plugin. If we find that it contains a plugin, we won't recurse any // further -- you can't have plugins within plugins. If we didn't find a plugin, we'll keep recursing. TArray PossiblePluginDirectories; { class FPluginDirectoryVisitor : public IPlatformFile::FDirectoryVisitor { public: FPluginDirectoryVisitor( TArray& InitFoundDirectories ) : FoundDirectories( InitFoundDirectories ) { } virtual bool Visit(const TCHAR* FilenameOrDirectory, bool bIsDirectory) override { if( bIsDirectory ) { FoundDirectories.Add( FString( FilenameOrDirectory ) ); } const bool bShouldContinue = true; return bShouldContinue; } TArray& FoundDirectories; }; FPluginDirectoryVisitor PluginDirectoryVisitor( PossiblePluginDirectories ); FPlatformFileManager::Get().GetPlatformFile().IterateDirectory( *PluginsDirectory, PluginDirectoryVisitor ); } for( auto PossiblePluginDirectoryIter( PossiblePluginDirectories.CreateConstIterator() ); PossiblePluginDirectoryIter; ++PossiblePluginDirectoryIter ) { const FString PossiblePluginDirectory = *PossiblePluginDirectoryIter; FString PluginDescriptorFilename; { // Usually the plugin descriptor is named the same as the directory. It doesn't have to match the directory // name but we'll check that first because its much faster than scanning! const FString ProbablePluginDescriptorFilename = PossiblePluginDirectory / (FPaths::GetCleanFilename(PossiblePluginDirectory) + PluginSystemDefs::PluginDescriptorFileExtension); if( FPlatformFileManager::Get().GetPlatformFile().FileExists( *ProbablePluginDescriptorFilename ) ) { PluginDescriptorFilename = ProbablePluginDescriptorFilename; } else { // Scan the directory for a plugin descriptor. TArray PluginDescriptorFilenames; { class FPluginDescriptorFileVisitor : public IPlatformFile::FDirectoryVisitor { public: FPluginDescriptorFileVisitor( const FWildcardString& InitSearchPattern, TArray& InitFoundPluginDescriptorFilenames ) : SearchPattern( InitSearchPattern ), FoundPluginDescriptorFilenames( InitFoundPluginDescriptorFilenames ) { } virtual bool Visit(const TCHAR* FilenameOrDirectory, bool bIsDirectory) override { bool bShouldContinue = true; if( !bIsDirectory ) { if( SearchPattern.IsMatch( FilenameOrDirectory ) ) { FoundPluginDescriptorFilenames.Add( FString( FilenameOrDirectory ) ); bShouldContinue = false; } } return bShouldContinue; } const FWildcardString& SearchPattern; TArray& FoundPluginDescriptorFilenames; }; const FWildcardString PluginDescriptorSearchString = PossiblePluginDirectory / FString(TEXT("*")) + PluginSystemDefs::PluginDescriptorFileExtension; FPluginDescriptorFileVisitor PluginDescriptorFileVisitor( PluginDescriptorSearchString, PluginDescriptorFilenames ); FPlatformFileManager::Get().GetPlatformFile().IterateDirectory( *PossiblePluginDirectory, PluginDescriptorFileVisitor ); } // Did we find any plugin descriptor files? if( PluginDescriptorFilenames.Num() > 0 ) { PluginDescriptorFilename = PluginDescriptorFilenames[ 0 ];; } } } if( !PluginDescriptorFilename.IsEmpty() ) { // Found a plugin directory! No need to recurse any further. // Load the descriptor file for this plugin into a new plugin info entry TSharedRef< FPlugin > NewPlugin = MakeShareable( new FPlugin() ); FText FailureReason; const bool bLoadedSuccessfully = NewPlugin->LoadFromFile(PluginDescriptorFilename, FailureReason); if( bLoadedSuccessfully ) { NewPlugin->SetLoadedFrom(LoadedFrom); if (FPaths::IsProjectFilePathSet()) { FString GameProjectFolder = FPaths::GetPath(FPaths::GetProjectFilePath()); if (PluginDescriptorFilename.StartsWith(GameProjectFolder)) { FString PluginBinariesFolder = FPaths::Combine(*FPaths::GetPath(PluginDescriptorFilename), TEXT("Binaries"), FPlatformProcess::GetBinariesSubdirectory()); FModuleManager::Get().AddBinariesDirectory(*PluginBinariesFolder, (LoadedFrom == FPluginInfo::ELoadedFrom::GameProject)); } } Plugins.Add(NewPlugin); } else { // NOTE: Even though loading of this plugin failed, we'll keep processing other plugins UE_LOG(LogPluginManager, Error, TEXT("%s"), *FailureReason.ToString()); } } else { // Didn't find a plugin in this directory. Continue to look in subfolders. FindPluginsRecursively( PossiblePluginDirectory, LoadedFrom, Plugins ); } } } public: /** * Searches for plugins and generates a list of plugin descriptors * * @param PluginsDirectory The base directory to search for plugins * @param LoadedFrom Where we're loading these plugins from (game, engine, etc) * @param Plugins The array to be filled in with loaded plugin descriptors */ static void FindPluginsIn( const FString& PluginsDirectory, const FPluginInfo::ELoadedFrom::Type LoadedFrom, TArray< TSharedRef >& Plugins ) { // Make sure the directory even exists if( FPlatformFileManager::Get().GetPlatformFile().DirectoryExists( *PluginsDirectory ) ) { FindPluginsRecursively( PluginsDirectory, LoadedFrom, Plugins ); } } }; { TArray< TSharedRef > Plugins; #if (WITH_ENGINE && !IS_PROGRAM) || WITH_PLUGIN_SUPPORT // Find "built-in" plugins. That is, plugins situated right within the Engine directory. Local::FindPluginsIn( FPaths::EnginePluginsDir(), FPluginInfo::ELoadedFrom::Engine, Plugins ); // Find plugins in the game project directory (/Plugins) if( FApp::HasGameName() ) { Local::FindPluginsIn( FPaths::GamePluginsDir(), FPluginInfo::ELoadedFrom::GameProject, Plugins ); } #endif // (WITH_ENGINE && !IS_PROGRAM) || WITH_PLUGIN_SUPPORT // Create plugin objects for all of the plugins that we found ensure( AllPlugins.Num() == 0 ); // Should not have already been initialized! AllPlugins = Plugins; for( auto PluginIt( Plugins.CreateConstIterator() ); PluginIt; ++PluginIt ) { const TSharedRef& Plugin = *PluginIt; const FPluginInfo& PluginInfo = Plugin->GetPluginInfo(); // Add the plugin binaries directory const FString PluginBinariesPath = FPaths::Combine(*FPaths::GetPath(PluginInfo.LoadedFromFile), TEXT("Binaries"), FPlatformProcess::GetBinariesSubdirectory()); FModuleManager::Get().AddBinariesDirectory(*PluginBinariesPath, PluginInfo.LoadedFrom == FPluginInfo::ELoadedFrom::GameProject); // Make sure to tell the module manager about any of our plugin's modules, so it will know where on disk to find them. for( auto ModuleInfoIt( PluginInfo.Modules.CreateConstIterator() ); ModuleInfoIt; ++ModuleInfoIt ) { const FProjectOrPluginInfo::FModuleInfo& ModuleInfo = *ModuleInfoIt; // @todo plugin: We're adding all modules always here, even editor modules which might not exist at runtime. Hope that's OK. FModuleManager::Get().AddModule(ModuleInfo.Name); // Add to the module->plugin map ModulePluginMap.Add( ModuleInfo.Name, Plugin ); } } } } void FPluginManager::EnablePluginsThatAreConfiguredToBeEnabled() { TArray< FString > EnabledPluginNames; GetPluginsConfiguredToBeEnabled(EnabledPluginNames); TSet< FString > AllEnabledPlugins; AllEnabledPlugins.Append( MoveTemp(EnabledPluginNames) ); for( const TSharedRef< FPlugin > Plugin : AllPlugins ) { if ( AllEnabledPlugins.Contains(Plugin->GetPluginName()) ) { Plugin->SetEnabled(true); } } } void FPluginManager::RegisterEnabledPluginMountPoints() { for( const TSharedRef< FPlugin > Plugin : AllPlugins ) { if ( Plugin->IsEnabled() ) { Plugin->RegisterPluginMountPoints( RegisterMountPointDelegate ); } } } void FPluginManager::LoadModulesForEnabledPlugins( const ELoadingPhase::Type LoadingPhase ) { if ( !bEarliestPhaseProcessed ) { EnablePluginsThatAreConfiguredToBeEnabled(); RegisterEnabledPluginMountPoints(); bEarliestPhaseProcessed = true; } // Load plugins! for( const TSharedRef< FPlugin > Plugin : AllPlugins ) { if ( Plugin->IsEnabled() ) { TMap ModuleLoadFailures; Plugin->LoadModules( LoadingPhase, ModuleLoadFailures ); FText FailureMessage; for( auto FailureIt( ModuleLoadFailures.CreateConstIterator() ); FailureIt; ++FailureIt ) { const auto ModuleNameThatFailedToLoad = FailureIt.Key(); const auto FailureReason = FailureIt.Value(); if( FailureReason != ELoadModuleFailureReason::Success ) { const FText PluginNameText = FText::FromString(Plugin->GetPluginName()); const FText TextModuleName = FText::FromName(FailureIt.Key()); if ( FailureReason == ELoadModuleFailureReason::FileNotFound ) { FailureMessage = FText::Format( LOCTEXT("PluginModuleNotFound", "Plugin '{0}' failed to load because module '{1}' could not be found. This plugin's functionality will not be available. Please ensure the plugin is properly installed, otherwise consider disabling the plugin for this project."), PluginNameText, TextModuleName ); } else if ( FailureReason == ELoadModuleFailureReason::FileIncompatible ) { FailureMessage = FText::Format( LOCTEXT("PluginModuleIncompatible", "Plugin '{0}' failed to load because module '{1}' does not appear to be compatible with the current version of the engine. This plugin's functionality will not be available. The plugin may need to be recompiled."), PluginNameText, TextModuleName ); } else if ( FailureReason == ELoadModuleFailureReason::CouldNotBeLoadedByOS ) { FailureMessage = FText::Format( LOCTEXT("PluginModuleCouldntBeLoaded", "Plugin '{0}' failed to load because module '{1}' could not be loaded. This plugin's functionality will not be available. There may be an operating system error or the module may not be properly set up."), PluginNameText, TextModuleName ); } else if ( FailureReason == ELoadModuleFailureReason::FailedToInitialize ) { FailureMessage = FText::Format( LOCTEXT("PluginModuleFailedToInitialize", "Plugin '{0}' failed to load because module '{1}' could be initialized successfully after it was loaded. This plugin's functionality will not be available."), PluginNameText, TextModuleName ); } else { ensure(0); // If this goes off, the error handling code should be updated for the new enum values! FailureMessage = FText::Format( LOCTEXT("PluginGenericLoadFailure", "Plugin '{0}' failed to load because module '{1}' could not be loaded for an unspecified reason. This plugin's functionality will not be available. Please report this error."), PluginNameText, TextModuleName ); } // Don't need to display more than one module load error per plugin that failed to load break; } } if( !FailureMessage.IsEmpty() ) { FMessageDialog::Open(EAppMsgType::Ok, FailureMessage); } } } } void FPluginManager::SetRegisterMountPointDelegate( const FRegisterMountPointDelegate& Delegate ) { RegisterMountPointDelegate = Delegate; } bool FPluginManager::IsPluginModule( const FName ModuleName ) const { return ( ModulePluginMap.Find( ModuleName ) != NULL ); } bool FPluginManager::AreEnabledPluginModulesUpToDate() { if (!bEarliestPhaseProcessed) { EnablePluginsThatAreConfiguredToBeEnabled(); RegisterEnabledPluginMountPoints(); bEarliestPhaseProcessed = true; } for (TArray< TSharedRef< FPlugin > >::TConstIterator Iter(AllPlugins); Iter; ++Iter) { const TSharedRef< FPlugin > &Plugin = *Iter; if (Plugin->IsEnabled() && !Plugin->AreModulesUpToDate()) { return false; } } return true; } void FPluginManager::GetPluginsConfiguredToBeEnabled(TArray& OutPluginNames) const { #if !IS_PROGRAM GConfig->GetArray( TEXT("Plugins"), TEXT("EnabledPlugins"), OutPluginNames, GEngineIni ); #else GConfig->GetArray(TEXT("Plugins"), TEXT("ProgramEnabledPlugins"), OutPluginNames, GEngineIni); #endif } void FPluginManager::ConfigurePluginToBeEnabled(const FString& PluginName, bool bEnabled) { TArray< FString > EnabledPluginNames; GetPluginsConfiguredToBeEnabled(EnabledPluginNames); bool bSaveEnabledPluginNames = false; if ( bEnabled ) { if ( !EnabledPluginNames.Contains(PluginName) ) { EnabledPluginNames.Add(PluginName); bSaveEnabledPluginNames = true; } } else { int32 ExistingEnabledIdx = EnabledPluginNames.Find(PluginName); if ( ExistingEnabledIdx != INDEX_NONE ) { EnabledPluginNames.RemoveAt(ExistingEnabledIdx); bSaveEnabledPluginNames = true; } } bRestartRequired = true; if ( bSaveEnabledPluginNames ) { GConfig->SetArray( TEXT("Plugins"), TEXT("EnabledPlugins"), EnabledPluginNames, GEngineIni ); GConfig->Flush(false); } } IPluginManager& IPluginManager::Get() { // Single instance of manager, allocated on demand and destroyed on program exit. static FPluginManager* PluginManager = NULL; if( PluginManager == NULL ) { PluginManager = new FPluginManager(); } return *PluginManager; } FPlugin::FPlugin() : bPluginEnabled( false ) { } void FPlugin::RegisterPluginMountPoints( const IPluginManager::FRegisterMountPointDelegate& RegisterMountPointDelegate ) { // If this plugin has content, mount it before loading the first module it contains // @todo plugin: Should we do this even if there is no Content directory? Probably, so that we can add content in Content Browser! if( PluginInfo.bCanContainContent ) { if( ensure( RegisterMountPointDelegate.IsBound() ) ) { // @todo plugin content: Consider prefixing path with "Plugins" or more specifically, either "EnginePlugins" or "GamePlugins", etc. // @todo plugin content: We may need the full relative path to the plugin under the respective Plugins directory if we want to fully avoid collisions const FString ContentRootPath( FString::Printf( TEXT( "/%s/" ), *PluginInfo.Name ) ); const FString PluginContentDirectory( FPaths::GetPath(PluginInfo.LoadedFromFile) / TEXT( "Content" ) ); RegisterMountPointDelegate.Execute( ContentRootPath, PluginContentDirectory ); } } } const FString& FPlugin::GetPluginName() const { return PluginInfo.Name; } void FPlugin::SetEnabled(bool bNewPluginEnabled) { bPluginEnabled = bNewPluginEnabled; } bool FPlugin::IsEnabled() const { return bPluginEnabled; } void FPlugin::SetLoadedFrom(FPluginInfo::ELoadedFrom::Type NewLoadedFrom) { PluginInfo.LoadedFrom = NewLoadedFrom; } bool FPlugin::PerformAdditionalDeserialization(const TSharedRef< FJsonObject >& FileObject) { ReadBoolFromJSON(FileObject, TEXT("CanContainContent"), PluginInfo.bCanContainContent); ReadBoolFromJSON(FileObject, TEXT("IsBetaVersion"), PluginInfo.bIsBetaVersion); return true; } void FPlugin::PerformAdditionalSerialization(const TSharedRef< TJsonWriter<> >& Writer) const { Writer->WriteValue(TEXT("CanContainContent"), PluginInfo.bCanContainContent); Writer->WriteValue(TEXT("IsBetaVersion"), PluginInfo.bIsBetaVersion); } TArray< FPluginStatus > FPluginManager::QueryStatusForAllPlugins() const { TArray< FPluginStatus > PluginStatuses; for( auto PluginIt( AllPlugins.CreateConstIterator() ); PluginIt; ++PluginIt ) { const TSharedRef< FPlugin >& Plugin = *PluginIt; const auto& PluginInfo = Plugin->GetPluginInfo(); FPluginStatus PluginStatus; PluginStatus.Name = PluginInfo.Name; PluginStatus.FriendlyName = PluginInfo.FriendlyName; PluginStatus.Version = PluginInfo.Version; PluginStatus.VersionName = PluginInfo.VersionName; PluginStatus.Description = PluginInfo.Description; PluginStatus.CreatedBy = PluginInfo.CreatedBy; PluginStatus.CreatedByURL = PluginInfo.CreatedByURL; PluginStatus.CategoryPath = PluginInfo.Category; PluginStatus.bIsEnabled = Plugin->IsEnabled(); PluginStatus.bIsBuiltIn = ( PluginInfo.LoadedFrom == FPluginInfo::ELoadedFrom::Engine ); PluginStatus.bIsBetaVersion = PluginInfo.bIsBetaVersion; // @todo plugedit: Maybe we should do the FileExists check ONCE at plugin load time and not at query time const FString Icon128FilePath = FPaths::GetPath(PluginInfo.LoadedFromFile) / PluginSystemDefs::RelativeIcon128FilePath; if( FPlatformFileManager::Get().GetPlatformFile().FileExists( *Icon128FilePath ) ) { PluginStatus.Icon128FilePath = Icon128FilePath; } PluginStatuses.Add( PluginStatus ); } return PluginStatuses; } void FPluginManager::SetPluginEnabled( const FString& PluginName, bool bEnabled ) { for( auto PluginIt( AllPlugins.CreateConstIterator() ); PluginIt; ++PluginIt ) { const TSharedRef< FPlugin >& Plugin = *PluginIt; if ( Plugin->GetPluginName() == PluginName ) { Plugin->SetEnabled(bEnabled); ConfigurePluginToBeEnabled(PluginName, bEnabled); break; } } } bool FPluginManager::IsRestartRequired() const { return bRestartRequired; } #undef LOCTEXT_NAMESPACE