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 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 Tasks = new(); private static readonly List ExportedDelegates = new(); private static readonly Dictionary CachedDirectoryTimes = new(); private static Dictionary _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? jsonValue = JsonSerializer.Deserialize>(fileStream); if (jsonValue != null) { _modulesWriteInfo = new Dictionary(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(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 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 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 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); })!); } } }