mirror of
https://github.com/Facepunch/sbox-public.git
synced 2025-12-23 22:48:07 -05:00
- Only download packages from the current project when opening in the editor - Make sure to download packages referenced within any libraries in the current project
404 lines
12 KiB
C#
404 lines
12 KiB
C#
using Sandbox.Engine;
|
|
using Sandbox.Engine.Shaders;
|
|
using Sandbox.Physics;
|
|
using System;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.Threading;
|
|
|
|
namespace Editor;
|
|
|
|
/// <summary>
|
|
/// Called once on editor startup to load the -project
|
|
/// </summary>
|
|
static class StartupLoadProject
|
|
{
|
|
public static bool IsLoading { get; private set; } = false;
|
|
|
|
public static Logger Log = new( "Startup" );
|
|
|
|
/// <summary>
|
|
/// Load the startup project for the first time
|
|
/// </summary>
|
|
public static async Task Run()
|
|
{
|
|
IsLoading = true;
|
|
|
|
// Create the editor window - hidden
|
|
new EditorMainWindow();
|
|
|
|
var path = Sandbox.Utility.CommandLine.GetSwitch( "-project", "" ).TrimQuoted();
|
|
|
|
Log.Info( $"Opening {path}" );
|
|
|
|
bool success = await OpenProject( path, default );
|
|
|
|
if ( !success )
|
|
{
|
|
EditorUtility.Quit( true );
|
|
return;
|
|
}
|
|
|
|
//
|
|
// Add to project list if not already there
|
|
//
|
|
{
|
|
var projectList = new ProjectList();
|
|
projectList.TryAddFromFile( path );
|
|
projectList.SaveList();
|
|
}
|
|
|
|
//
|
|
// We don't need you no more
|
|
//
|
|
Editor.EditorSplashScreen.StartupFinish();
|
|
|
|
//
|
|
// We're ready - open the editor window
|
|
//
|
|
EditorWindow.Startup();
|
|
|
|
// Show build notifications by default for any compilers from now on
|
|
CompileGroup.SuppressBuildNotifications = false;
|
|
|
|
IsLoading = false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Opens the project for the editor, this should only be called once from the -project command line.
|
|
/// </summary>
|
|
static async Task<bool> OpenProject( string path, CancellationToken ct )
|
|
{
|
|
if ( string.IsNullOrEmpty( path ) ) throw new ArgumentException( nameof( path ) );
|
|
Assert.IsNull( Project.Current );
|
|
|
|
// should never be one existing once we remove all this shit
|
|
Project project = Project.AddFromFile( path, false );
|
|
var parentPackage = project.Config.GetMetaOrDefault<string>( "ParentPackage", null );
|
|
|
|
using ( var _ = Bootstrap.StartupTiming?.ScopeTimer( $"Load Project: Init FileSystem" ) )
|
|
{
|
|
FileSystem.InitializeFromProject( project );
|
|
}
|
|
|
|
ProjectCookie?.Dispose();
|
|
ProjectCookie = new CookieContainer( "project", fileSystem: FileSystem.ProjectTemporary );
|
|
|
|
// Set the project as active and sync with the package manager to install any dependant packages the compiler depends on
|
|
Project.Current = project;
|
|
Project.Current.LastOpened = DateTime.Now;
|
|
project.Active = true;
|
|
using ( var _ = Bootstrap.StartupTiming?.ScopeTimer( $"Load Project: Init AssetSystem" ) )
|
|
{
|
|
AssetSystem.InitializeFromProject( project );
|
|
}
|
|
|
|
// Scan the project for libraries
|
|
using ( var _ = Bootstrap.StartupTiming?.ScopeTimer( $"Load Project: Init Libraries" ) )
|
|
{
|
|
LibrarySystem.InitializeFromProject( project );
|
|
}
|
|
|
|
//
|
|
// We want to load all the built in projects before anything else. This gives us a baseline.
|
|
// Then if our "parentpackage" has a "base" package, it'll hotload over our baseline.
|
|
//
|
|
Log.Info( $"Load Builtin Projects" );
|
|
using ( var _ = Bootstrap.StartupTiming?.ScopeTimer( $"Load Project: Builtin Projects" ) )
|
|
{
|
|
await PackageManager.InstallProjects( Project.All.Where( x => x.IsBuiltIn ).ToArray() );
|
|
}
|
|
|
|
//
|
|
// Load the dlls etc from our parent package
|
|
//
|
|
if ( project.Config.Type == "addon" && !string.IsNullOrWhiteSpace( parentPackage ) )
|
|
{
|
|
using ( var _ = Bootstrap.StartupTiming?.ScopeTimer( $"Load Project: ParentPackage" ) )
|
|
{
|
|
Log.Info( $"Load {parentPackage}" );
|
|
await PackageManager.InstallAsync( new PackageLoadOptions( parentPackage, "tools" ) );
|
|
}
|
|
}
|
|
|
|
//
|
|
// This should really only load our current project, that we're editing right now
|
|
//
|
|
Log.Info( $"Syncing package manager" );
|
|
using ( var _ = Bootstrap.StartupTiming?.ScopeTimer( $"Load Project: Sync PackageManager" ) )
|
|
{
|
|
await Project.SyncWithPackageManager();
|
|
}
|
|
|
|
|
|
// This double Load shit is stupid, creates the compilers properly now that we've installed any dependant packages
|
|
project.Load();
|
|
|
|
ExportSettings( project );
|
|
|
|
Log.Info( $"Generating solution" );
|
|
using ( var _ = Bootstrap.StartupTiming?.ScopeTimer( $"Load Project: Generate Solution" ) )
|
|
{
|
|
await Project.GenerateSolution();
|
|
}
|
|
|
|
Log.Info( $"Creating Filesystem" );
|
|
using ( var _ = Bootstrap.StartupTiming?.ScopeTimer( $"Load Project: Update ProjectFilesystem" ) )
|
|
{
|
|
UpdateProjectFilesystem( project );
|
|
}
|
|
|
|
// Compiles and waits for the project in a bullshit way - this already starts happening way sooner
|
|
Log.Info( $"Compiling projects" );
|
|
|
|
if ( await EditorUtility.Projects.Updated( project ) == false )
|
|
{
|
|
using var _ = Bootstrap.StartupTiming?.ScopeTimer( $"Load Project: Compile" );
|
|
// load failed, present the user with the information
|
|
// and let them decide if they want to continue broken or bail (or fix things, recompile and continue)
|
|
|
|
if ( await StartupFailedPopup.Show( project ) == false )
|
|
return false;
|
|
|
|
await EditorUtility.Projects.WaitForCompiles();
|
|
}
|
|
|
|
if ( project.Config.Type == "addon" && !string.IsNullOrWhiteSpace( parentPackage ) )
|
|
{
|
|
using var _ = Bootstrap.StartupTiming?.ScopeTimer( $"Load Project: Load Addon" );
|
|
|
|
Log.Info( $"LoadGamePackageAsync" );
|
|
// Install it as the active game
|
|
await GameInstanceDll.Current.LoadGamePackageAsync( parentPackage, GameLoadingFlags.Host | GameLoadingFlags.Reload, ct );
|
|
|
|
Log.Info( $"AssetSystem.InstallAsync" );
|
|
// Install into asset system so we can use prefabs, gameresources, etc.
|
|
await AssetSystem.InstallAsync( parentPackage, false );
|
|
|
|
Log.Info( $"MountAsync" );
|
|
// Mount our current project, and load the source!
|
|
await project.Package.MountAsync( true );
|
|
|
|
// Mount our current project into the filesystem and make sure to load all assets
|
|
FileSystem.Mounted.CreateAndMount( project.GetAssetsPath() );
|
|
ResourceLoader.LoadAllGameResource( FileSystem.Mounted );
|
|
}
|
|
else
|
|
{
|
|
using var _ = Bootstrap.StartupTiming?.ScopeTimer( $"Load Project: Load Game" );
|
|
|
|
// It'd be nice to do a full end to end test, but this is as far as it'll go
|
|
if ( Sandbox.Application.IsUnitTest )
|
|
return true;
|
|
|
|
Log.Info( $"Loading game package" );
|
|
await GameInstanceDll.Current.LoadGamePackageAsync( project.Package.FullIdent, GameLoadingFlags.Host | GameLoadingFlags.Reload, ct );
|
|
}
|
|
|
|
|
|
using ( var _ = Bootstrap.StartupTiming?.ScopeTimer( $"Load Project: Init Mounts" ) )
|
|
{
|
|
// Mount any mounts that are required
|
|
await EditorUtility.Mounting.InitMountsFromConfig( project );
|
|
}
|
|
|
|
//
|
|
// Load the resources
|
|
//
|
|
Log.Info( "Importing custom assets" );
|
|
using ( var _ = Bootstrap.StartupTiming?.ScopeTimer( $"Load Project: Register CustomAssetTypes" ) )
|
|
{
|
|
AssetType.UpdateCustomTypes();
|
|
AssetType.ImportCustomTypeFiles();
|
|
}
|
|
|
|
// Download cloud assets afterwards, otherwise we don't have references
|
|
Log.Info( "Refreshing cloud assets" );
|
|
using ( var _ = Bootstrap.StartupTiming?.ScopeTimer( $"Load Project: DownloadCloudAssets" ) )
|
|
{
|
|
await RefreshCloudAssets( ct );
|
|
}
|
|
|
|
// Go through and compile all assets
|
|
using ( var _ = Bootstrap.StartupTiming?.ScopeTimer( $"Load Project: CompileAllAssets" ) )
|
|
{
|
|
await CompileAllShaders();
|
|
CompileAllAssets();
|
|
|
|
FileWatch.Tick();
|
|
|
|
// do we even need this anymore?
|
|
IAssetSystem.LoadWorkingSetsAndTags();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static void UpdateProjectFilesystem( Project project )
|
|
{
|
|
var assetsPath = project.GetAssetsPath();
|
|
if ( !System.IO.Directory.Exists( assetsPath ) )
|
|
return;
|
|
|
|
NativeEngine.FullFileSystem.AddProjectPath( project.Config.FullIdent, project.GetAssetsPath() );
|
|
|
|
var cloudFolder = System.IO.Path.Combine( project.GetRootPath(), ".sbox", "cloud" );
|
|
NativeEngine.FullFileSystem.AddCloudPath( "mod_cloud", cloudFolder );
|
|
|
|
var transientFolder = System.IO.Path.Combine( project.GetRootPath(), ".sbox", "transient" );
|
|
NativeEngine.FullFileSystem.AddCloudPath( "mod_transient", transientFolder );
|
|
|
|
//
|
|
// The engine ships a bunch of transient files, like image generations from the addon base, and
|
|
// cloud assets that the menu scene uses. Mount them last, but no need in the menu project.
|
|
//
|
|
if ( project.Config.Ident != "menu" )
|
|
{
|
|
var engineTransient = EngineFileSystem.Root.GetFullPath( "addons/menu/transients" );
|
|
NativeEngine.FullFileSystem.AddCloudPath( "mod_engtrans", engineTransient );
|
|
}
|
|
|
|
Editor.FileSystem.RebuildContentPath();
|
|
|
|
if ( !Sandbox.Application.IsUnitTest )
|
|
{
|
|
IAssetSystem.UpdateMods();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// At some point we'll figure out how to do resources so they'll compile properly.
|
|
/// Until then.. this is what we got.
|
|
/// </summary>
|
|
static async Task CompileAllShaders()
|
|
{
|
|
var projectDir = Path.GetFullPath( Project.Current.RootDirectory.FullName );
|
|
|
|
var sw = Stopwatch.StartNew();
|
|
var gr = AssetSystem.All.Where( x => x.AssetType == AssetType.Shader && x.HasSourceFile && Path.GetFullPath( x.AbsoluteSourcePath ).StartsWith( projectDir, StringComparison.OrdinalIgnoreCase ) ).ToArray();
|
|
if ( gr.Length == 0 ) return;
|
|
|
|
var options = new ShaderCompileOptions();
|
|
options.ForceRecompile = false;
|
|
options.SingleThreaded = false;
|
|
options.ConsoleOutput = false;
|
|
|
|
FastTimer timer = FastTimer.StartNew();
|
|
foreach ( var r in gr )
|
|
{
|
|
await ShaderCompile.Compile( r.AbsolutePath, r.RelativePath, options, default );
|
|
}
|
|
if ( sw.Elapsed.TotalSeconds > 2 )
|
|
{
|
|
Log.Info( $"Compiling shaders took {sw.Elapsed.TotalSeconds:0.000}s" );
|
|
}
|
|
}
|
|
|
|
static void CompileAllAssets()
|
|
{
|
|
var sw = Stopwatch.StartNew();
|
|
var gr = AssetSystem.All.Where( x => !x.IsTrivialChild && x.CanRecompile && !x.IsCompiledAndUpToDate ).ToArray();
|
|
if ( gr.Length == 0 ) return;
|
|
|
|
FastTimer timer = FastTimer.StartNew();
|
|
|
|
int i = 0;
|
|
foreach ( var r in gr )
|
|
{
|
|
i++;
|
|
Log.Info( $"Compiling {i}/{gr.Length} {r.Path}" );
|
|
IToolsDll.Current?.Spin();
|
|
r.Compile( false );
|
|
}
|
|
|
|
if ( sw.Elapsed.TotalSeconds > 2 )
|
|
{
|
|
Log.Info( $"Compiling assets took {sw.Elapsed.TotalSeconds:0.000}s" );
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Download all required cloud resources for each asset in the asset system, and remove any we don't need anymore
|
|
/// </summary>
|
|
static async Task RefreshCloudAssets( CancellationToken token )
|
|
{
|
|
var sw = Stopwatch.StartNew();
|
|
|
|
packagesToDownload.AddRange( CloudAsset.GetAssetReferences( true ) );
|
|
|
|
// 1. remove any installed packages we no longer need
|
|
var required = new HashSet<string>();
|
|
if ( IGameInstance.Current?.Package is RemotePackage remotePackage )
|
|
{
|
|
// may require a game package, don't uninstall that
|
|
required.Add( remotePackage.FullIdent );
|
|
}
|
|
|
|
foreach ( var ident in packagesToDownload )
|
|
{
|
|
// normalise into org.ident, without version, so we can easily check below
|
|
if ( Package.TryParseIdent( ident, out var p ) )
|
|
required.Add( Package.FormatIdent( p.org, p.package ) );
|
|
}
|
|
|
|
foreach ( var package in AssetSystem.GetInstalledPackages() )
|
|
{
|
|
if ( required.Contains( package.FullIdent ) ) continue;
|
|
|
|
Log.Info( $"'{package.FullIdent}' is unused, cleaning up.." );
|
|
AssetSystem.UninstallPackage( package );
|
|
}
|
|
|
|
// 2. make sure everything we need is installed
|
|
await CloudAsset.Install( packagesToDownload.Distinct(), token );
|
|
|
|
Log.Info( $"..refresh complete (took {sw.Elapsed.TotalSeconds:0.000}s)" );
|
|
}
|
|
|
|
private static void ExportSettings( Project project )
|
|
{
|
|
if ( !FileSystem.ProjectSettings.FileExists( "/Input.config" ) )
|
|
{
|
|
if ( !project.Config.TryGetMeta<InputSettings>( "InputSettings", out var meta ) )
|
|
{
|
|
meta = new InputSettings();
|
|
}
|
|
|
|
FileSystem.ProjectSettings.WriteJson( "/Input.config", meta.Serialize() );
|
|
}
|
|
|
|
if ( project.Config.SetMeta( "InputSettings", null ) )
|
|
{
|
|
project.Save();
|
|
}
|
|
|
|
if ( !FileSystem.ProjectSettings.FileExists( "/Collision.config" ) )
|
|
{
|
|
if ( !project.Config.TryGetMeta<CollisionRules>( "Collision", out var meta ) )
|
|
{
|
|
meta = new CollisionRules();
|
|
}
|
|
|
|
EditorUtility.SaveProjectSettings( meta, "/Collision.config" );
|
|
}
|
|
|
|
if ( project.Config.SetMeta( "Collision", null ) )
|
|
{
|
|
project.Save();
|
|
}
|
|
}
|
|
|
|
static List<string> packagesToDownload = new();
|
|
|
|
/// <summary>
|
|
/// Adds a package to packagesToDownload - which will be downloaded
|
|
/// </summary>
|
|
internal static void QueuePackageDownload( string packageName )
|
|
{
|
|
if ( !IsLoading )
|
|
throw new System.Exception( "QueuePackageDownload shouldn't get called after startup" );
|
|
|
|
packagesToDownload.Add( packageName );
|
|
}
|
|
}
|