using Sandbox.Engine; using System; using System.Threading; namespace Editor; public static class LibrarySystem { static HashSet _all = new(); static string LibraryFolder { get; set; } /// /// Get all active libraries /// static public IEnumerable All => _all; /// /// Scan this project's Libraries folder and add all of the library projects from it. /// internal static void InitializeFromProject( Project project ) { LibraryFolder = System.IO.Path.Combine( project.RootDirectory.FullName, "Libraries" ); foreach ( var folder in System.IO.Directory.EnumerateDirectories( LibraryFolder ) ) { AddFromFolder( folder ); } } /// /// Add a library project from a specific folder /// private static LibraryProject AddFromFolder( string folder ) { var configs = System.IO.Directory.EnumerateFiles( folder, "*.sbproj" ).ToArray(); if ( configs.Length != 1 ) return default; var project = Project.AddFromFile( configs[0], true ); if ( project is null ) return default; var existing = _all.SingleOrDefault( x => x.Project == project ); if ( existing is not null ) { // Re-read the version - in case it updated existing.ReadVersionFromFile(); return existing; } var lib = new LibraryProject( project ); _all.Add( lib ); return lib; } /// /// Add a library from this folder /// public static async Task Add( string folderName, CancellationToken token ) { var fullPath = FileSystem.Libraries.GetFullPath( folderName ); if ( !System.IO.Directory.Exists( fullPath ) ) { Log.Warning( "Tried to add library but it doesn't exist" ); return; } var project = AddFromFolder( fullPath ); if ( project is null ) { Log.Warning( "Tried to add library but it couldn't create project" ); return; } // If we have an assets path, then AddFromFolder called the LibraryProject constructor // which added a new CONTENT path. So here we tell the engine to rebuild the content // paths, so it finds all the new assets. if ( project.Project.HasAssetsPath() ) { Editor.FileSystem.RebuildContentPath(); if ( !Sandbox.Application.IsUnitTest ) { IAssetSystem.UpdateMods(); } ResourceLoader.LoadAllGameResource( FileSystem.Mounted ); } // install it await PackageManager.InstallAsync( new PackageLoadOptions( project.Project.Package.FullIdent, "local", token ) ); await PackageManager.InstallAsync( new PackageLoadOptions( project.Project.Package.FullIdent, "gamemenu", token ) ); // update the sln with the new projects await Project.GenerateSolution(); // I don't know if this does anything? IAssetSystem.UpdateMods(); // Trigger a rebuild of all projects and wait for the result await EditorUtility.Projects.Updated( Project.Current ); // Recreate the project filesystem //FileSystem.InitializeFromProject( Project.Current ); // Reload the game project, which'll load the new library too await GameInstanceDll.Current.LoadGamePackageAsync( Project.Current.Package.FullIdent, GameLoadingFlags.Host | GameLoadingFlags.Reload, token ); } internal static void RemoveLibrary( LibraryProject proj ) { _all.Remove( proj ); var path = proj.Project.GetRootPath(); DeleteFolderWithRetry( path ); EditorUtility.DisplayDialog( "Library Removed", "This library has been deleted. Please restart the editor to fully remove it. This is horrible I am so sorry." ); } private static void DeleteFolderWithRetry( string path ) { // try to delete the folder a few times // expect trouble, so retry and ignore some errors for ( int i = 0; i < 10; i++ ) { try { System.IO.Directory.Delete( path, true ); } catch ( System.IO.DirectoryNotFoundException ) { // Good - that's what we want break; } catch ( System.Exception ) { Thread.Sleep( 100 ); } } } [ConCmd( "library_install", ConVarFlags.Protected )] internal static void InstallLibrary( string name ) { if ( !Package.TryParseIdent( name, out var ident ) ) { Log.Warning( "Invalid package ident" ); return; } Log.Warning( $"Installing Library {ident.org}.{ident.package}" ); _ = Install( $"{ident.org}.{ident.package}", ident.version ?? -1 ); } /// /// Install a library from a package. This will download the package and install it in the project's Library folder. /// public static async Task Install( string ident, long versionId = -1, CancellationToken token = default ) { // // TODO - We need a scoped thing to prevent filesystem reacting to file changes // something like using( FileSystem.DeferChanges() ), but globally // using var progress = Progress.Start( "Installing Package" ); var package = await Package.FetchAsync( ident, false ); if ( package is null ) { Log.Warning( "Failed to install library, package not found" ); return false; } // no version specified - then use the current live version if ( versionId <= 0 ) { versionId = package.Revision.VersionId; } // get the version info var version = (await Package.FetchVersions( ident )).Where( x => x.VersionId == versionId ).FirstOrDefault(); if ( version == null ) { Log.Warning( "Failed to install library, version wasn't found" ); return false; } var folderName = package.FullIdent; var versionPath = $"{folderName}/.version"; var versionIdent = new Version( 1, 0, (int)version.VersionId ); // Download the manifest await version.DownloadManifestAsync( token ); // TODO: Remove this once we have FileSystem.DeferChanges() FileWatch.SuppressWatchers = RealTime.Now + 999; // does this already exist? if ( FileSystem.Libraries.FileExists( versionPath ) ) { var versionText = FileSystem.Libraries.ReadAllText( versionPath ); if ( Version.TryParse( versionText.Trim(), out var result ) ) { if ( versionIdent == result ) { Log.Warning( "Updating library to the same version. Erasing local changes." ); } else { Log.Info( $"Updating library from {result} to {versionIdent}" ); } } // loop over existing files and delete any with changes or that are no longer in the manifest var existingFiles = FileSystem.Libraries.FindFile( folderName, "*", true ); foreach ( var existingFile in existingFiles ) { var newFile = version.Manifest.Files.FirstOrDefault( x => x.Path == existingFile ); var fullPath = $"{folderName}/{existingFile}"; if ( !version.Manifest.Files.Any( x => x.Path == existingFile ) || FileSystem.Libraries.GetCrc( fullPath ) != Convert.ToUInt64( newFile.Crc, 16 ) ) { FileSystem.Libraries.DeleteFile( fullPath ); Progress.Update( $"Deleting {existingFile}" ); } } } FileSystem.Libraries.CreateDirectory( folderName ); // Write the version id FileSystem.Libraries.WriteAllText( versionPath, versionIdent.ToString() ); // Download each new file (this is done in parallel, attempting to DL up to 10mb at once) var filesToDownload = new List(); var filesDownloading = new List(); foreach ( var file in version.Manifest.Files ) { var libraryPath = $"{folderName}/{file.Path}"; if ( FileSystem.Libraries.FileExists( libraryPath ) ) { continue; } filesToDownload.Add( file ); } long bytesDownloading = 0; while ( filesToDownload.Count > 0 ) { if ( bytesDownloading < 10_000 && filesToDownload.Count > 0 ) { var file = filesToDownload[0]; filesToDownload.RemoveAt( 0 ); Parallel.Invoke( async () => { filesDownloading.Add( file ); bytesDownloading += file.Size; var libraryPath = $"{folderName}/{file.Path}"; var targetPath = FileSystem.Libraries.GetFullPath( libraryPath ); await EditorUtility.DownloadAsync( file.Url, targetPath, null, token ); Progress.Update( file.Path ); bytesDownloading -= file.Size; filesDownloading.Remove( file ); } ); } await Task.Delay( 1 ); } FileWatch.SuppressWatchers = RealTime.Now; await Add( folderName, token ); return true; } } public class LibraryProject { public Version Version { get; set; } public Project Project { get; } internal LibraryProject( Project project ) { Project = project; ReadVersionFromFile(); // add the content path if ( project.HasAssetsPath() ) { var assetPath = project.GetAssetsPath(); // Add to our c# content search path FileSystem.Content.CreateAndMount( assetPath ); FileSystem.Mounted.CreateAndMount( assetPath ); // make content available to the game EngineFileSystem.LibraryContent.CreateAndMount( assetPath ); // Add to the c++ filesystem search path NativeEngine.FullFileSystem.AddProjectPath( project.Config.FullIdent, project.GetAssetsPath() ); } if ( project.HasCodePath() ) { // make .scss etc available to the game EngineFileSystem.LibraryContent.CreateAndMount( project.GetCodePath() ); } } /// /// Try to read the library version from /.version /// internal void ReadVersionFromFile() { Version = new Version( 1, 1, 1 ); var versionInfo = System.IO.Path.Combine( Project.GetRootPath(), ".version" ); if ( !System.IO.File.Exists( versionInfo ) ) return; var versionText = System.IO.File.ReadAllText( versionInfo ); if ( Version.TryParse( versionText.Trim(), out var result ) ) { Version = result; } } /// /// Remove and delete this library, and folder /// public void RemoveAndDelete() { LibrarySystem.RemoveLibrary( this ); } }