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 );
}
}
}