Files
2026-07-13 13:13:17 +08:00

319 lines
14 KiB
C#

#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;
}
}
/// <summary>
/// Loads all projects and packages - for use only at the start of the editor
/// </summary>
/// <param name="forceRecompile">all symbol projects will be recompiled if possible</param>
/// <exception cref="Exception">Unknown exceptions may be raised - if you want to handle them, wrap this in a try/catch</exception>
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
}
/// <summary>
/// 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 <see cref="Symbol.HasUnresolvedChildren"/>), so nothing is lost.
/// </summary>
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.");
}
/// <summary>
/// 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.
/// </summary>
private static void WarnAboutCorruptedSymbolFiles(EditorSymbolPackage[] packages)
{
var corrupted = new List<string>();
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);
/// <summary>
/// Load each project file and its associated assembly
/// </summary>
private static void LoadProjects(FileInfo[] csProjFiles, bool forceRecompile, out List<ProjectLoadInfo> 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<string> 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, "..", "..", "..", "..");
}