using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using System.Text.Json; namespace Sandbox; public class CodeArchive { /// /// The name of the compiler /// public string CompilerName { get; set; } /// /// The compiler's configuration settings /// public Compiler.Configuration Configuration { get; set; } /// /// The syntax trees that should be compiled /// public List SyntaxTrees { get; } = new(); /// /// Represents a file to send to the compiler along with all the code. This is usually /// something that the generator turns into code, such as a Razor file. /// public record AdditionalFile( string Text, string LocalPath ); /// /// Additional files that the compiler/generator needs. This is going to be .razor files. /// public List AdditionalFiles { get; } = new(); /// /// Converts the syntax tree paths from physical paths to project local paths /// public Dictionary FileMap { get; } = new( StringComparer.OrdinalIgnoreCase ); /// /// References that this compiler/generator needs to compile the code /// public HashSet References { get; } = new HashSet( StringComparer.OrdinalIgnoreCase ); /// /// The version of the code archive /// 1005 - Inital version /// 1006 - Razor updates. Add razor namespaces on older versions. /// 1007 - Razor changed to our own Microsoft.AspNetCore.Components assembly /// public long Version { get; set; } /// /// If true then we shouldn't automatically add @namespace to razor output, we shouldn't /// use the folder namespace structure. /// internal bool Version_UsesOldRazorNamespaces => Version < 1007; public CodeArchive() { Version = 1007; } public CodeArchive( byte[] data ) { Deserialize( data ); } /// /// Serialize to a byte array /// public byte[] Serialize() { using ByteStream bs = ByteStream.Create( 128 ); bs.Write( "GMCA" ); // gmod code archive bs.Write( Version ); // version bs.Write( CompilerName ); bs.Write( JsonSerializer.Serialize( Configuration ) ); bs.Write( JsonSerializer.Serialize( References ) ); bs.Write( JsonSerializer.Serialize( SyntaxTrees.Select( x => new AdditionalFile( x.GetText().ToString(), x.FilePath ) ) ) ); bs.Write( JsonSerializer.Serialize( AdditionalFiles ) ); bs.Write( JsonSerializer.Serialize( FileMap ) ); return bs.Compress().ToArray(); } /// /// Deserialize from a byte array /// internal void Deserialize( byte[] data ) { using var compressed = ByteStream.CreateReader( data ); using var bs = compressed.Decompress(); var ident = bs.Read(); if ( ident != "GMCA" ) throw new Exception( "Invalid archive" ); Version = bs.Read(); if ( Version < 1005 ) throw new Exception( "Invalid archive version" ); if ( Version > 1007 ) throw new Exception( "Invalid archive version" ); CompilerName = bs.Read(); Configuration = JsonSerializer.Deserialize( bs.Read() ); // parse the syntaxtree's using the same options as the creator var parseOptions = Configuration.GetParseOptions(); References.Clear(); var references = JsonSerializer.Deserialize>( bs.Read() ); foreach ( string reference in references ) { // 7th June 2025 - We removed Sandbox.Game, which held the scene and ui systems. They're in engine now. if ( reference.Equals( "Sandbox.Game", StringComparison.OrdinalIgnoreCase ) ) continue; References.Add( reference ); } // 14th Nov 2025 - Older archives didn't reference Microsoft.AspNetCore.Components, but they should now. if ( Version < 1007 ) { References.Add( "Microsoft.AspNetCore.Components" ); } SyntaxTrees.Clear(); var syntaxTrees = JsonSerializer.Deserialize>( bs.Read() ); foreach ( var file in syntaxTrees ) { var snytaxTree = CSharpSyntaxTree.ParseText( file.Text, path: file.LocalPath, encoding: System.Text.Encoding.UTF8, options: parseOptions ); SyntaxTrees.Add( snytaxTree ); } AdditionalFiles.Clear(); var additionalFiles = JsonSerializer.Deserialize>( bs.Read() ); foreach ( var file in additionalFiles ) { AdditionalFiles.Add( file ); } var fileMap = JsonSerializer.Deserialize>( bs.Read() ); FileMap.Clear(); foreach ( (string k, string v) in fileMap ) { FileMap[k] = v; } if ( Version < 1006 ) { // In archives previous to 1006 we didn't define a global using for global using Microsoft.AspNetCore.Components // and after that we moved the razor stuff into that namespace to be compatible with visual studio. var tree = CSharpSyntaxTree.ParseText( "global using Microsoft.AspNetCore.Components; \nglobal using Microsoft.AspNetCore.Components.Rendering;\n", path: "__gen_RazorNamespace.cs", encoding: System.Text.Encoding.UTF8, options: parseOptions ); SyntaxTrees.Add( tree ); } } }