using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.Json; namespace Sandbox.SolutionGenerator { public class Generator { List Projects = new(); public ProjectInfo AddProject( string type, string packageIdent, string name, string path, Compiler.Configuration settings ) { var project = new ProjectInfo( type, packageIdent, name, path, settings ); Projects.Add( project ); return project; } private string NormalizePath( string path ) => new Uri( path ).LocalPath; private string AttemptAbsoluteToRelative( string basePath, string relativePath, int maxDepth = 5 ) { string baseNormalized = NormalizePath( basePath ); string relativeNormalized = NormalizePath( relativePath ); string baseEnding = string.Empty; if ( Path.HasExtension( baseNormalized ) ) { baseEnding = Path.GetFileName( baseNormalized ); baseNormalized = NormalizePath( baseNormalized.Substring( 0, baseNormalized.Length - baseEnding.Length ) ); } if ( Path.HasExtension( relativeNormalized ) ) { relativeNormalized = Path.GetDirectoryName( relativeNormalized ); } string finalPath = Path.GetRelativePath( relativeNormalized, basePath ); // Exceed how far we want our relative path to go, bail out and use original path if ( finalPath.Split( ".." ).Length > maxDepth ) { if ( baseEnding == null ) { return baseNormalized; } else { return Path.Combine( baseNormalized, baseEnding ); } } else { if ( baseEnding == null ) { return finalPath; } else { return Path.Combine( finalPath, baseEnding ); } } } private static readonly JsonSerializerOptions JsonWriteIndented = new() { WriteIndented = true }; public void Run( string gameExePath, string managedFolder, string solutionPath, string relativePath, string projectPath ) { string normalizedRelativePath = NormalizePath( projectPath ); int relativePathoffset = normalizedRelativePath.Length + 1; managedFolder = Path.Combine( relativePath, managedFolder ); solutionPath = Path.Combine( projectPath, solutionPath ); gameExePath = Path.Combine( relativePath, gameExePath ); foreach ( var p in Projects ) { var csproj = new Project { ProjectName = p.Name, ProjectReferences = "", ManagedRoot = AttemptAbsoluteToRelative( managedFolder, p.CsprojPath ), GameRoot = AttemptAbsoluteToRelative( relativePath, p.CsprojPath ), References = p.References, GlobalStatic = p.GlobalStatic, GlobalUsing = p.GlobalUsing, RootNamespace = p.Settings.RootNamespace ?? "Sandbox", Nullable = p.Settings.Nullables ? "enable" : "disable", NoWarn = p.Settings.NoWarn, WarningsAsErrors = p.Settings.WarningsAsErrors, TreatWarningsAsErrors = p.Settings.TreatWarningsAsErrors, DefineConstants = p.Settings.DefineConstants, Unsafe = p.Type == "tool", IgnoreFolders = p.Settings.IgnoreFolders.ToList(), IsEditorProject = p.IsEditorProject, IsUnitTestProject = p.IsUnitTestProject, IgnoreFiles = p.IgnoreFiles }; foreach ( var proj in p.PackageReferences.Distinct().Order() ) { if ( proj.Contains( "\\" ) ) { csproj.ProjectReferences += $" \n"; continue; } var reference = Projects.FirstOrDefault( x => x.Name == proj || x.PackageIdent == proj ); if ( reference != null ) { var path = NormalizePath( $"{reference.Path}\\{reference.Name}.csproj" ); csproj.ProjectReferences += $" \n"; } else { csproj.ProjectReferences += $" \" />\n"; new Sandbox.Diagnostics.Logger( "SolutionGenerator" ).Warning( $"Couldn't find project '{proj}' for {csproj.ProjectName} to reference" ); } } WriteTextIfChanged( p.CsprojPath, csproj.TransformText() ); if ( gameExePath != null && !p.IsUnitTestProject ) { var propertiesPath = Path.Combine( p.Path, "Properties" ); Directory.CreateDirectory( propertiesPath ); var launchSettings = new LaunchSettings { Profiles = new() }; launchSettings.Profiles.Add( "Editor", new LaunchSettings.Profile { CommandName = "Executable", ExecutablePath = Path.Combine( relativePath, "sbox-dev.exe" ), CommandLineArgs = $"-project \"{p.SandboxProjectFilePath}\"", } ); WriteTextIfChanged( Path.Combine( propertiesPath, "launchSettings.json" ), JsonSerializer.Serialize( launchSettings, JsonWriteIndented ) ); } } // Build solution var slnx = new Solution(); foreach ( var p in Projects ) { string normalizedProjectPath = NormalizePath( p.CsprojPath ); if ( normalizedProjectPath.StartsWith( normalizedRelativePath ) ) { normalizedProjectPath = normalizedProjectPath.Substring( relativePathoffset ); } normalizedProjectPath = normalizedProjectPath.Trim( '/', '\\' ); slnx.AddProject( normalizedProjectPath, p.Folder ); } WriteTextIfChanged( solutionPath, slnx.Generate() ); // Some vscode files { var vscodePath = Path.Combine( projectPath, ".vscode" ); var extensions = new VSCodeExtensions { Recommendations = ["ms-dotnettools.csdevkit", "shader-slang.slang-language-extension"] }; WriteTextIfChanged( Path.Combine( vscodePath, "extensions.json" ), JsonSerializer.Serialize( extensions, JsonWriteIndented ) ); var settings = new VSCodeSettings(); // Don't use Unity's Shaderlab please settings.FilesAssociations = new() { { "*.shader", "slang" }, { "*.hlsl", "slang" } }; var shaderSearchPaths = new List { Path.Combine( relativePath, "core", "shaders" ) }; foreach ( var p in Projects ) { var rootPath = Path.GetDirectoryName( p.SandboxProjectFilePath ); shaderSearchPaths.Add( Path.Combine( rootPath, "Assets", "shaders" ) ); } settings.SlangIncludePaths = shaderSearchPaths.ToArray(); WriteTextIfChanged( Path.Combine( vscodePath, "settings.json" ), JsonSerializer.Serialize( settings, JsonWriteIndented ) ); } } private static void WriteTextIfChanged( string path, string contents ) { try { if ( File.Exists( path ) ) { var existingContents = File.ReadAllText( path ); if ( contents == existingContents ) return; } } catch { } var folder = Path.GetDirectoryName( path ); if ( !string.IsNullOrEmpty( folder ) && !Directory.Exists( folder ) ) Directory.CreateDirectory( folder ); File.WriteAllText( path, contents ); } } }