347 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
			
		
		
	
	
			347 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
| using System;
 | |
| using System.Collections.Generic;
 | |
| using System.IO;
 | |
| using System.Linq;
 | |
| using System.Reflection;
 | |
| using System.Text.Json;
 | |
| using System.Threading.Tasks;
 | |
| using EpicGames.Core;
 | |
| using EpicGames.UHT.Types;
 | |
| using EpicGames.UHT.Utils;
 | |
| using UnrealSharpScriptGenerator.Exporters;
 | |
| using UnrealSharpScriptGenerator.Model;
 | |
| using UnrealSharpScriptGenerator.PropertyTranslators;
 | |
| using UnrealSharpScriptGenerator.Utilities;
 | |
| 
 | |
| namespace UnrealSharpScriptGenerator;
 | |
| 
 | |
| class ModuleFolders
 | |
| {
 | |
|     public Dictionary<string, DateTime> DirectoryToWriteTime { get; set; } = new();
 | |
|     public bool HasBeenExported;
 | |
| }
 | |
| 
 | |
| public static class CSharpExporter
 | |
| {
 | |
|     const string ModuleDataFileName = "UnrealSharpModuleData.json";
 | |
|     private const string SpecialtypesJson = "SpecialTypes.json";
 | |
|     public static bool HasModifiedEngineGlue;
 | |
| 
 | |
|     private static readonly List<Task> Tasks = new();
 | |
|     private static readonly List<string> ExportedDelegates = new();
 | |
|     private static readonly Dictionary<string, DateTime> CachedDirectoryTimes = new();
 | |
|     private static Dictionary<string, ModuleFolders?> _modulesWriteInfo = new();
 | |
| 
 | |
|     public static void StartExport()
 | |
|     {
 | |
|         if (!HasChangedGeneratorSourceRecently())
 | |
|         {
 | |
|             // The source for this generator hasn't changed, so we don't need to re-export the whole API.
 | |
|             DeserializeModuleData();
 | |
|         }
 | |
|         else
 | |
|         {
 | |
|             // Just in case the source has changed, we need to clean the old files
 | |
|             Console.WriteLine("Detected new source since last export, cleaning old files...");
 | |
|             FileExporter.CleanModuleFolders();
 | |
|         }
 | |
| 
 | |
|         Console.WriteLine("Exporting C++ to C#...");
 | |
| 
 | |
|         #if UE_5_5_OR_LATER
 | |
|         foreach (UhtModule module in Program.Factory.Session.Modules)
 | |
|         {
 | |
|             foreach (UhtPackage modulePackage in module.Packages)
 | |
|             {
 | |
|                 ExportPackage(modulePackage);
 | |
|             }
 | |
|         }
 | |
|         #else
 | |
|         foreach (UhtPackage package in Program.Factory.Session.Packages)
 | |
|         {
 | |
|             ExportPackage(package);
 | |
|         }
 | |
|         #endif
 | |
| 
 | |
|         WaitForTasks();
 | |
| 
 | |
|         FunctionExporter.StartExportingExtensionMethods(Tasks);
 | |
| 
 | |
|         WaitForTasks();
 | |
| 
 | |
|         AutocastExporter.StartExportingAutocastFunctions(Tasks);
 | |
| 
 | |
|         WaitForTasks();
 | |
| 
 | |
|         SerializeModuleData();
 | |
| 
 | |
|         string generatedCodeDirectory = Program.PluginModule.OutputDirectory;
 | |
|         string typeInfoFilePath = Path.Combine(generatedCodeDirectory, SpecialtypesJson);
 | |
|         OutputTypeRules(typeInfoFilePath);
 | |
|     }
 | |
| 
 | |
|     static void DeserializeModuleData()
 | |
|     {
 | |
|         if (!Directory.Exists(Program.EngineGluePath))
 | |
|         {
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         string outputPath = Path.Combine(Program.PluginModule.OutputDirectory, ModuleDataFileName);
 | |
| 
 | |
|         if (!File.Exists(outputPath))
 | |
|         {
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         using FileStream fileStream = new FileStream(outputPath, FileMode.Open, FileAccess.Read, FileShare.Read);
 | |
|         Dictionary<string, ModuleFolders>? jsonValue = JsonSerializer.Deserialize<Dictionary<string, ModuleFolders>>(fileStream);
 | |
| 
 | |
|         if (jsonValue != null)
 | |
|         {
 | |
|             _modulesWriteInfo = new Dictionary<string, ModuleFolders?>(jsonValue!);
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     static void SerializeModuleData()
 | |
|     {
 | |
|         string outputPath = Path.Combine(Program.PluginModule.OutputDirectory, ModuleDataFileName);
 | |
|         using FileStream fs = new FileStream(outputPath, FileMode.Create, FileAccess.Write);
 | |
|         JsonSerializer.Serialize(fs, _modulesWriteInfo);
 | |
|     }
 | |
| 
 | |
|     static bool HasChangedGeneratorSourceRecently()
 | |
|     {
 | |
|         string executingAssemblyPath = Assembly.GetExecutingAssembly().Location;
 | |
|         DateTime executingAssemblyLastWriteTime = File.GetLastWriteTimeUtc(executingAssemblyPath);
 | |
| 
 | |
|         string generatedCodeDirectory = Program.PluginModule.OutputDirectory;
 | |
|         string timestampFilePath = Path.Combine(generatedCodeDirectory, "Timestamp");
 | |
|         string typeInfoFilePath = Path.Combine(generatedCodeDirectory, SpecialtypesJson);
 | |
| 
 | |
|         if (!File.Exists(timestampFilePath) || !File.Exists(typeInfoFilePath) || !Directory.Exists(Program.EngineGluePath))
 | |
|         {
 | |
|             return true;
 | |
|         }
 | |
| 
 | |
|         if (TypeRulesChanged(typeInfoFilePath))
 | |
|         {
 | |
|             return true;
 | |
|         }
 | |
| 
 | |
|         DateTime savedTimestampUtc = File.GetLastWriteTimeUtc(timestampFilePath);
 | |
|         return executingAssemblyLastWriteTime > savedTimestampUtc;
 | |
|     }
 | |
| 
 | |
|     static bool TypeRulesChanged(string typeInfoFilePath)
 | |
|     {
 | |
|         using var fs = new FileStream(typeInfoFilePath, FileMode.Open, FileAccess.Read, FileShare.Read);
 | |
|         var rules = JsonSerializer.Deserialize<SpecialTypeInfo>(fs);
 | |
|         if (rules == null)
 | |
|         {
 | |
|             return true;
 | |
|         }
 | |
| 
 | |
|         return !rules.Equals(PropertyTranslatorManager.SpecialTypeInfo);
 | |
|     }
 | |
| 
 | |
|     static void OutputTypeRules(string typeInfoFilePath)
 | |
|     {
 | |
|         using var fs = new FileStream(typeInfoFilePath, FileMode.Create, FileAccess.Write);
 | |
|         JsonSerializer.Serialize(fs, PropertyTranslatorManager.SpecialTypeInfo);
 | |
|     }
 | |
| 
 | |
|     private static void WaitForTasks()
 | |
|     {
 | |
|         Task[] waitTasks = Tasks.ToArray();
 | |
|         if (waitTasks.Length > 0)
 | |
|         {
 | |
|             Task.WaitAll(waitTasks);
 | |
|         }
 | |
|         Tasks.Clear();
 | |
|     }
 | |
| 
 | |
|     private static void ExportPackage(UhtPackage package)
 | |
|     {
 | |
|         if (!package.ShouldExport())
 | |
|         {
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         if (!Program.BuildingEditor && package.PackageFlags.HasAnyFlags(EPackageFlags.EditorOnly | EPackageFlags.UncookedOnly))
 | |
|         {
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         if (!package.IsPartOfEngine())
 | |
|         {
 | |
|             package.FindOrAddProjectInfo();
 | |
|         }
 | |
| 
 | |
|         string packageName = package.GetShortName();
 | |
| 
 | |
|         if (!_modulesWriteInfo.TryGetValue(packageName, out ModuleFolders? lastEditTime))
 | |
|         {
 | |
|             lastEditTime = new ModuleFolders();
 | |
|             _modulesWriteInfo.Add(packageName, lastEditTime);
 | |
|         }
 | |
| 
 | |
|         HashSet<string> processedDirectories = new();
 | |
| 
 | |
|         string generatedPath = FileExporter.GetDirectoryPath(package);
 | |
|         bool doesDirectoryExist = Directory.Exists(generatedPath);
 | |
| 
 | |
|         foreach (UhtType child in package.Children)
 | |
|         {
 | |
|             string directoryName = Path.GetDirectoryName(child.HeaderFile.FilePath)!;
 | |
| 
 | |
|             // We only need to export the C++ directory if it doesn't exist or if it has been modified
 | |
|             if (!doesDirectoryExist || ShouldExportDirectory(directoryName, lastEditTime!))
 | |
|             {
 | |
|                 processedDirectories.Add(directoryName);
 | |
|                 ForEachChild(child, ExportType);
 | |
|             }
 | |
|             else
 | |
|             {
 | |
|                 ForEachChild(child, FileExporter.AddUnchangedType);
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         if (processedDirectories.Count == 0)
 | |
|         {
 | |
|             // No directories in this package have been exported or modified
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         // The glue has been exported, so we need to update the last write times
 | |
|         UpdateLastWriteTimes(processedDirectories, lastEditTime!);
 | |
|     }
 | |
| 
 | |
|     private static void ForEachChild(UhtType child, Action<UhtType> action)
 | |
|     {
 | |
|         #if UE_5_5_OR_LATER
 | |
|         action(child);
 | |
| 
 | |
|         foreach (UhtType type in child.Children)
 | |
|         {
 | |
|             action(type);
 | |
|         }
 | |
|         #else
 | |
|         foreach (UhtType type in child.Children)
 | |
|         {
 | |
|             action(type);
 | |
| 
 | |
|             foreach (UhtType innerType in type.Children)
 | |
|             {
 | |
|                 action(innerType);
 | |
|             }
 | |
|         }
 | |
|         #endif
 | |
| 
 | |
|     }
 | |
| 
 | |
|     public static bool HasBeenExported(string directory)
 | |
|     {
 | |
|         return _modulesWriteInfo.TryGetValue(directory, out ModuleFolders? lastEditTime) && lastEditTime is
 | |
|         {
 | |
|             HasBeenExported: true
 | |
|         };
 | |
|     }
 | |
| 
 | |
|     private static bool ShouldExportDirectory(string directoryPath, ModuleFolders lastEditTime)
 | |
|     {
 | |
|         if (!CachedDirectoryTimes.TryGetValue(directoryPath, out DateTime cachedTime))
 | |
|         {
 | |
|             DateTime currentWriteTime = Directory.GetLastWriteTimeUtc(directoryPath);
 | |
|             CachedDirectoryTimes[directoryPath] = currentWriteTime;
 | |
|             cachedTime = currentWriteTime;
 | |
|         }
 | |
| 
 | |
|         return !lastEditTime.DirectoryToWriteTime.TryGetValue(directoryPath, out DateTime lastEditTimeValue) || lastEditTimeValue != cachedTime;
 | |
|     }
 | |
| 
 | |
|     private static void UpdateLastWriteTimes(HashSet<string> directories, ModuleFolders lastEditTime)
 | |
|     {
 | |
|         foreach (string directory in directories)
 | |
|         {
 | |
|             if (!CachedDirectoryTimes.TryGetValue(directory, out DateTime cachedTime))
 | |
|             {
 | |
|                 continue;
 | |
|             }
 | |
| 
 | |
|             lastEditTime.DirectoryToWriteTime[directory] = cachedTime;
 | |
|             lastEditTime.HasBeenExported = true;
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     private static void ExportType(UhtType type)
 | |
|     {
 | |
|         if (type.HasMetadata(PackageUtilities.SkipGlueGenerationDefine) 
 | |
|             || PropertyTranslatorManager.SpecialTypeInfo.Structs.SkippedTypes.Contains(type.SourceName))
 | |
|         {
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         bool isManualExport = PropertyTranslatorManager.SpecialTypeInfo.Structs.BlittableTypes.ContainsKey(type.SourceName);
 | |
| 
 | |
|         if (type is UhtClass classObj)
 | |
|         {
 | |
|             if (classObj.HasAllFlags(EClassFlags.Interface))
 | |
|             {
 | |
|                 if (isManualExport)
 | |
|                 {
 | |
|                     return;
 | |
|                 }
 | |
| 
 | |
|                 if (classObj.ClassType is not UhtClassType.Interface && type != Program.Factory.Session.IInterface)
 | |
|                 {
 | |
|                     return;
 | |
|                 }
 | |
| 
 | |
|                 Tasks.Add(Program.Factory.CreateTask(_ => { InterfaceExporter.ExportInterface(classObj); })!);
 | |
|             }
 | |
|             else
 | |
|             {
 | |
|                 Tasks.Add(Program.Factory.CreateTask(_ => { ClassExporter.ExportClass(classObj, isManualExport); })!);
 | |
|             }
 | |
|         }
 | |
|         else if (type is UhtEnum enumObj)
 | |
|         {
 | |
|             if (isManualExport)
 | |
|             {
 | |
|                 return;
 | |
|             }
 | |
| 
 | |
|             Tasks.Add(Program.Factory.CreateTask(_ => { EnumExporter.ExportEnum(enumObj); })!);
 | |
|         }
 | |
|         else if (type is UhtScriptStruct structObj)
 | |
|         {
 | |
|             isManualExport = PropertyTranslatorManager.SpecialTypeInfo.Structs.BlittableTypes.TryGetValue(structObj.SourceName, out var info) && info.ManagedType is not null;
 | |
|             Tasks.Add(Program.Factory.CreateTask(_ => { StructExporter.ExportStruct(structObj, isManualExport); })!);
 | |
|         }
 | |
|         else if (type.EngineType == UhtEngineType.Delegate)
 | |
|         {
 | |
|             if (isManualExport)
 | |
|             {
 | |
|                 return;
 | |
|             }
 | |
| 
 | |
|             UhtFunction delegateFunction = (UhtFunction) type;
 | |
|             if (!ScriptGeneratorUtilities.CanExportParameters(delegateFunction) || delegateFunction.ReturnProperty != null)
 | |
|             {
 | |
|                 return;
 | |
|             }
 | |
| 
 | |
|             // There are some duplicate delegates in the same modules, so we need to check if we already exported it
 | |
|             string delegateName = DelegateBasePropertyTranslator.GetFullDelegateName(delegateFunction);
 | |
|             if (ExportedDelegates.Contains(delegateName))
 | |
|             {
 | |
|                 return;
 | |
|             }
 | |
| 
 | |
|             ExportedDelegates.Add(delegateName);
 | |
|             Tasks.Add(Program.Factory.CreateTask(_ => { DelegateExporter.ExportDelegate(delegateFunction); })!);
 | |
|         }
 | |
|     }
 | |
| }
 |