#nullable enable using System.Diagnostics.CodeAnalysis; using System.IO; using T3.Core.Compilation; using T3.Core.IO; using T3.Core.Model; using T3.Core.Resource.Assets; using T3.Core.Settings; using T3.Editor.Gui.Interaction.StartupCheck; using T3.Editor.Gui.Interaction.Variations.Model; using T3.Editor.Gui.UiHelpers; using T3.Editor.UiModel; namespace T3.Editor.Compilation; internal static partial class ProjectSetup { internal static bool TryLoadAll(bool forceRecompile, [NotNullWhen(false)] out Exception? exception) { try { LoadAll(forceRecompile); exception = null; return true; } catch (Exception e) { exception = e; return false; } } /// /// Loads all projects and packages - for use only at the start of the editor /// /// all symbol projects will be recompiled if possible /// Unknown exceptions may be raised - if you want to handle them, wrap this in a try/catch private static void LoadAll(bool forceRecompile) { // ReSharper disable once RedundantAssignment bool isDebugBuild = false; #if DEBUG isDebugBuild = true; System.Diagnostics.Stopwatch totalStopwatch = new(); totalStopwatch.Start(); #endif if (!isDebugBuild) { // Load pre-built built-in packages as read-only LoadBuiltInPackages(); } // Find project files var csProjFiles = FindCsProjFiles(isDebugBuild); // Load projects LoadProjects(csProjFiles, forceRecompile, out var failedProjects); // Keep projects that failed to load visible and recoverable instead of silently dropping them // — a single broken project shouldn't just vanish from the Hub. BrokenProjects.Clear(); foreach (var failed in failedProjects) { // Archived projects also land in failedProjects (success, but intentionally not loaded); // only genuine failures are "broken". if (failed.success) continue; BrokenProjects.Add(new BrokenProjectInfo(failed.fileInfo, failed.csProjFile, failed.failureReason ?? "The project could not be loaded.", failed.hint)); } // Phase 1: Initial Startup Migration // This happens only once here and not in subsequent UpdateSymbolPackages calls foreach (var package in _activePackages) { if (ConformAssetPaths.RenameResourcesToAssets(package)) { Log.Debug($"Rescanning {package.Name} assets after migration..."); AssetRegistry.RegisterAssetsFromPackage(package); } } // Register UI types UiRegistration.RegisterUiTypes(); var allPackages = _activePackages.ToArray(); // Update all symbol packages UpdateSymbolPackages(allPackages); WarnAboutUnresolvedChildren(allPackages); WarnAboutCorruptedSymbolFiles(allPackages); // Needs registered symbols to resolve which project owns each variation file VariationsMigration.MigrateLegacyVariationsToPackageMeta(); // Initialize resources and shader linting Log.Info("Initializing package resources..."); foreach (var package in allPackages) { InitializePackageResources(package); } // FIXME: This needs to be properly handled. //ShaderLinter.AddPackage(SharedResources.ResourcePackage, ResourceManager.SharedShaderPackages); // Initialize custom UIs if (CoreSettings.Config.LogAssemblyLoadingDetails) { foreach (var package in SymbolPackage.AllPackages) { Log.Debug($"Completed loading {package.DisplayName}"); } } #if DEBUG totalStopwatch.Stop(); Log.Debug($">> Total load time: {totalStopwatch.ElapsedMilliseconds/1000:0.0}s"); #endif } /// /// Summarises symbols that lost children on load (missing package) into one clear warning, since /// the per-child console warnings are just raw Guids and easy to miss. These symbols are protected /// from being saved over (see ), so nothing is lost. /// private static void WarnAboutUnresolvedChildren(EditorSymbolPackage[] packages) { var affectedSymbols = 0; var totalChildren = 0; foreach (var package in packages) { foreach (var symbol in package.Symbols.Values) { if (!symbol.HasUnresolvedChildren) continue; affectedSymbols++; totalChildren += symbol.UnresolvedChildCount; } } if (affectedSymbols == 0) return; Log.Warning($"{totalChildren} operator(s) across {affectedSymbols} symbol(s) could not be loaded — " + "most likely a missing package. Those symbols are protected: the editor will not save over " + "them, so nothing is lost. Install the missing package(s) and reload to restore them."); } /// /// Summarises symbol files that were corrupt and skipped during load (so one bad .t3 no longer /// aborts the whole editor). The affected packages refuse to save until reloaded, so the corrupt — /// but backup-recoverable — files aren't overwritten. /// private static void WarnAboutCorruptedSymbolFiles(EditorSymbolPackage[] packages) { var corrupted = new List(); foreach (var package in packages) corrupted.AddRange(package.CorruptedSymbolFilePaths); if (corrupted.Count == 0) return; Log.Warning($"{corrupted.Count} operator file(s) are corrupt and were skipped so the rest of the editor " + "could load. The affected project(s) will not save until you restore an earlier backup:\n " + string.Join("\n ", corrupted)); } private static void LoadBuiltInPackages() { var directory = Directory.CreateDirectory(_coreOperatorDirectory); directory .EnumerateDirectories("*", SearchOption.TopDirectoryOnly) .Where(folder => !folder.Name.EndsWith(FileLocations.ExportSubFolder, StringComparison.OrdinalIgnoreCase)) // ignore "player" project directory .ToList() .ForEach(directoryInfo => { AddToLoadedPackages((new EditorSymbolPackage(new AssemblyInformation(directoryInfo.FullName), null))); }); } private readonly record struct ProjectLoadInfo( FileInfo fileInfo, CsProjectFile? csProjFile, bool success, string? failureReason = null, string? hint = null); /// /// Load each project file and its associated assembly /// private static void LoadProjects(FileInfo[] csProjFiles, bool forceRecompile, out List failedProjects) { Log.Info("Loading projects..."); // Load each project file and its associated assembly var projectResults = csProjFiles .AsParallel() .Select(fileInfo => { if (!CsProjectFile.TryLoad(fileInfo, out var loadInfo)) { Log.Error($"Failed to load project at \"{fileInfo.FullName}\":\n{loadInfo.Error}"); return new ProjectLoadInfo(fileInfo, null, false, "The project file could not be read.", loadInfo.Error); } var csProjFile = loadInfo.CsProjectFile!; // Check if archived before doing anything else if (csProjFile.IsArchived) { lock (ArchivedProjects) { ArchivedProjects.Add(new ArchivedProjectInfo(csProjFile)); } return new ProjectLoadInfo(fileInfo, csProjFile, true); // Mark as success but don't process further } var needsCompile = forceRecompile || loadInfo.NeedsRecompile || !Directory.Exists(csProjFile.GetBuildTargetDirectory()); if (needsCompile && !csProjFile.TryRecompile(true, out var failureLog)) { Log.Error($"Failed to recompile project '{csProjFile.Name}:\n{failureLog}'"); var explanation = Compiler.ExplainBuildFailure(failureLog); if (explanation != null) Log.Warning($"Likely cause for '{csProjFile.Name}' compile failure:\n{explanation}"); return new ProjectLoadInfo(fileInfo, csProjFile, false, "The project failed to compile.", explanation ?? "See the log for details."); } return new ProjectLoadInfo(fileInfo, csProjFile, true); }) .ToArray(); failedProjects = []; foreach (var projectInfo in projectResults) { if (projectInfo is { csProjFile: not null, success: true }&& !projectInfo.csProjFile.IsArchived) { var project = new EditableSymbolProject(projectInfo.csProjFile); AddToLoadedPackages(project); } else { failedProjects.Add(projectInfo); } } } private static FileInfo[] FindCsProjFiles(bool includeBuiltInAsProjects) { return GetProjectDirectories(includeBuiltInAsProjects) .SelectMany(dir => Directory.EnumerateFiles(dir, "*.csproj", SearchOption.AllDirectories)) .Select(x => new FileInfo(x)) .ToArray(); static IEnumerable GetProjectDirectories(bool includeBuiltInAsProjects) { // ReSharper disable once JoinDeclarationAndInitializer string[] topDirectories = []; if (UserSettings.Config.EnableUsbProjectDetection) { var usbs = DriveInfo.GetDrives() .Where(drive => drive is { DriveType: DriveType.Removable, IsReady: true }); foreach (var usb in usbs) { var usbT3ProjectsPath = Path.Combine(usb.RootDirectory!.FullName, "TiXLProjects"); if (Directory.Exists(usbT3ProjectsPath)) { topDirectories = topDirectories.Append(usbT3ProjectsPath).ToArray(); } } } foreach (var projectPath in UserSettings.Config.ProjectDirectories) { if (!string.IsNullOrWhiteSpace(projectPath) && Directory.Exists(projectPath)) { topDirectories = topDirectories.Append(projectPath).ToArray(); } } var projectSearchDirectories = topDirectories .Where(Directory.Exists) .SelectMany(Directory.EnumerateDirectories) .Where(dirName => !dirName.Contains(FileLocations.ExportSubFolder, StringComparison.OrdinalIgnoreCase)); // Add Built-in packages as projects if (includeBuiltInAsProjects) { projectSearchDirectories = projectSearchDirectories.Concat(Directory.EnumerateDirectories(Path.Combine(_t3ParentDirectory, FileLocations.OperatorsSubFolder)) .Where(path => { var subDir = Path.GetFileName(path); return !subDir .StartsWith('.'); // ignore things like .git and file sync folders })); } return projectSearchDirectories; } } private static readonly string _coreOperatorDirectory = Path.Combine(FileLocations.StartFolder, FileLocations.OperatorsSubFolder); private static readonly string _t3ParentDirectory = Path.Combine(FileLocations.StartFolder, "..", "..", "..", ".."); }