diff --git a/UET/Redpoint.Logging.Mac/Redpoint.Logging.Mac.csproj b/UET/Redpoint.Logging.Mac/Redpoint.Logging.Mac.csproj index 32f0ab30..b36e5b60 100644 --- a/UET/Redpoint.Logging.Mac/Redpoint.Logging.Mac.csproj +++ b/UET/Redpoint.Logging.Mac/Redpoint.Logging.Mac.csproj @@ -14,13 +14,15 @@ true + * + $(PackageVersion) - + diff --git a/UET/Redpoint.Uet.BuildPipeline.Executors.GitLab/Redpoint.Uet.BuildPipeline.Executors.GitLab.csproj b/UET/Redpoint.Uet.BuildPipeline.Executors.GitLab/Redpoint.Uet.BuildPipeline.Executors.GitLab.csproj index 1c6789fa..169c96bd 100644 --- a/UET/Redpoint.Uet.BuildPipeline.Executors.GitLab/Redpoint.Uet.BuildPipeline.Executors.GitLab.csproj +++ b/UET/Redpoint.Uet.BuildPipeline.Executors.GitLab/Redpoint.Uet.BuildPipeline.Executors.GitLab.csproj @@ -4,7 +4,7 @@ - + diff --git a/UET/Redpoint.Uet.Patching.Runtime/ConsoleUetPatchLogging.cs b/UET/Redpoint.Uet.Patching.Runtime/ConsoleUetPatchLogging.cs new file mode 100644 index 00000000..2f4149f0 --- /dev/null +++ b/UET/Redpoint.Uet.Patching.Runtime/ConsoleUetPatchLogging.cs @@ -0,0 +1,20 @@ +namespace Redpoint.Uet.Patching.Runtime +{ + internal class ConsoleUetPatchLogging : IUetPatchLogging + { + public void LogInfo(string message) + { + Console.WriteLine($"Redpoint.Uet.Patching.Runtime [info ] {message}"); + } + + public void LogWarning(string message) + { + Console.WriteLine($"Redpoint.Uet.Patching.Runtime [warn ] {message}"); + } + + public void LogError(string message) + { + Console.WriteLine($"Redpoint.Uet.Patching.Runtime [error] {message}"); + } + } +} diff --git a/UET/Redpoint.Uet.Patching.Runtime/IUetPatchLogging.cs b/UET/Redpoint.Uet.Patching.Runtime/IUetPatchLogging.cs new file mode 100644 index 00000000..a3fb99f6 --- /dev/null +++ b/UET/Redpoint.Uet.Patching.Runtime/IUetPatchLogging.cs @@ -0,0 +1,11 @@ +namespace Redpoint.Uet.Patching.Runtime +{ + internal interface IUetPatchLogging + { + void LogInfo(string message); + + void LogWarning(string message); + + void LogError(string message); + } +} diff --git a/UET/Redpoint.Uet.Patching.Runtime/NullUetPatchLogging.cs b/UET/Redpoint.Uet.Patching.Runtime/NullUetPatchLogging.cs new file mode 100644 index 00000000..f3d84b8c --- /dev/null +++ b/UET/Redpoint.Uet.Patching.Runtime/NullUetPatchLogging.cs @@ -0,0 +1,17 @@ +namespace Redpoint.Uet.Patching.Runtime +{ + internal class NullUetPatchLogging : IUetPatchLogging + { + public void LogInfo(string message) + { + } + + public void LogWarning(string message) + { + } + + public void LogError(string message) + { + } + } +} diff --git a/UET/Redpoint.Uet.Patching.Runtime/Patches/UeDeployAndroidExternalFilesDirUetPatch.cs b/UET/Redpoint.Uet.Patching.Runtime/Patches/UeDeployAndroidExternalFilesDirUetPatch.cs new file mode 100644 index 00000000..ba2412da --- /dev/null +++ b/UET/Redpoint.Uet.Patching.Runtime/Patches/UeDeployAndroidExternalFilesDirUetPatch.cs @@ -0,0 +1,44 @@ +namespace Redpoint.Uet.Patching.Runtime.Patches +{ + using HarmonyLib; + using System.Reflection; + using System.Runtime.Loader; + + internal class UeDeployAndroidExternalFilesDirUetPatch : IUetPatch + { + private static IUetPatchLogging? _logging; + + public bool ShouldApplyPatch() + { + return Assembly.GetEntryAssembly()?.GetName()?.Name == "UnrealBuildTool"; + } + + public void ApplyPatch(IUetPatchLogging logging, Harmony harmony) + { + // Get the UnrealBuildTool.dll assembly. + var loadedUbtAssembly = AssemblyLoadContext.Default.Assemblies.First(x => x.GetName().Name == "UnrealBuildTool"); + + // Find the UseExternalFilesDir method. + var method = loadedUbtAssembly.GetType("UnrealBuildTool.UEDeployAndroid")?.GetDeclaredMethods() + .Where(x => x.Name == "UseExternalFilesDir") + .FirstOrDefault(); + if (method == null) + { + throw new InvalidOperationException("Unable to find UnrealBuildTool.UEDeployAndroid.UseExternalFilesDir method!"); + } + + // Store the logger so we can emit logs when overriding the disallow parameter. + _logging = logging; + + // Apply the prefix patch to always set 'bDisallowExternalFilesDir' to false. + var prefix = GetType().GetMethod(nameof(OverrideDisallowExternalFilesDir), AccessTools.all); + harmony.Patch(method, prefix: new HarmonyMethod(prefix)); + } + + public static void OverrideDisallowExternalFilesDir(ref bool bDisallowExternalFilesDir) + { + _logging?.LogInfo("Intercepting 'UseExternalFilesDir' method and setting 'bDisallowExternalFilesDir' to false."); + bDisallowExternalFilesDir = false; + } + } +} diff --git a/UET/Redpoint.Uet.Patching.Runtime/Patches/UetPatch.cs b/UET/Redpoint.Uet.Patching.Runtime/Patches/UetPatch.cs new file mode 100644 index 00000000..f2a3212e --- /dev/null +++ b/UET/Redpoint.Uet.Patching.Runtime/Patches/UetPatch.cs @@ -0,0 +1,12 @@ +namespace Redpoint.Uet.Patching.Runtime.Patches +{ + using HarmonyLib; + using Redpoint.Uet.Patching.Runtime; + + internal interface IUetPatch + { + bool ShouldApplyPatch(); + + void ApplyPatch(IUetPatchLogging logging, Harmony harmony); + } +} diff --git a/UET/Redpoint.Uet.Patching.Runtime/Redpoint.Uet.Patching.Runtime.csproj b/UET/Redpoint.Uet.Patching.Runtime/Redpoint.Uet.Patching.Runtime.csproj new file mode 100644 index 00000000..172471d5 --- /dev/null +++ b/UET/Redpoint.Uet.Patching.Runtime/Redpoint.Uet.Patching.Runtime.csproj @@ -0,0 +1,17 @@ + + + + net6.0 + enable + enable + true + + + + + + + + + + diff --git a/UET/Redpoint.Uet.Patching.Runtime/StartupHook.cs b/UET/Redpoint.Uet.Patching.Runtime/StartupHook.cs new file mode 100644 index 00000000..60f317a6 --- /dev/null +++ b/UET/Redpoint.Uet.Patching.Runtime/StartupHook.cs @@ -0,0 +1,23 @@ +using System.Reflection; +using System.Runtime.Loader; + +internal class StartupHook +{ + public static void Initialize() + { + // Set our assembly resolver for Mono.Cecil and other things. + var ourDirectoryPath = new FileInfo(Assembly.GetExecutingAssembly().Location).DirectoryName!; + AssemblyLoadContext.Default.Resolving += (AssemblyLoadContext context, AssemblyName name) => + { + var targetAssembly = Path.Combine(ourDirectoryPath, name.Name + ".dll"); + if (File.Exists(targetAssembly)) + { + return Assembly.LoadFrom(targetAssembly); + } + return null; + }; + + // Then call our real code. + Redpoint.Uet.Patching.Runtime.UetStartupHook.Initialize(); + } +} diff --git a/UET/Redpoint.Uet.Patching.Runtime/UetStartupHook.cs b/UET/Redpoint.Uet.Patching.Runtime/UetStartupHook.cs new file mode 100644 index 00000000..810b74cc --- /dev/null +++ b/UET/Redpoint.Uet.Patching.Runtime/UetStartupHook.cs @@ -0,0 +1,49 @@ +namespace Redpoint.Uet.Patching.Runtime +{ + using HarmonyLib; + using Microsoft.CodeAnalysis; + using Microsoft.CodeAnalysis.CSharp; + using Microsoft.CodeAnalysis.Text; + using Redpoint.Uet.Patching.Runtime.Patches; + using System; + using System.Reflection; + using System.Runtime.Loader; + + public static class UetStartupHook + { + public static void Initialize() + { + // Prevent MSBuild from re-using nodes, since we're injected into all .NET processes (including MSBuild). + Environment.SetEnvironmentVariable("MSBUILDDISABLENODEREUSE", "1"); + + // Our list of patches. + var patches = new IUetPatch[] + { + new UeDeployAndroidExternalFilesDirUetPatch(), + }; + + // Determine if we have any patches to apply. If we have none, we're done. + if (!patches.Any(x => x.ShouldApplyPatch())) + { + return; + } + + // Create our logging instance. + IUetPatchLogging logging = Environment.GetEnvironmentVariable("UET_RUNTIME_PATCHING_ENABLE_LOGGING") == "1" + ? new ConsoleUetPatchLogging() + : new NullUetPatchLogging(); + + // Create our Harmony instance. + var harmony = new Harmony("games.redpoint.uet"); + + // Go through our patches and apply the ones that are relevant. + foreach (var patch in patches) + { + if (patch.ShouldApplyPatch()) + { + patch.ApplyPatch(logging, harmony); + } + } + } + } +} diff --git a/UET/Redpoint.Uet.Uat/DefaultUATExecutor.cs b/UET/Redpoint.Uet.Uat/DefaultUATExecutor.cs index 0bc3e059..867501b8 100644 --- a/UET/Redpoint.Uet.Uat/DefaultUATExecutor.cs +++ b/UET/Redpoint.Uet.Uat/DefaultUATExecutor.cs @@ -227,6 +227,8 @@ await File.ReadAllTextAsync(scriptModuleFullName, cancellationToken).ConfigureAw while (true); } + // Extract the hooks and set DOTNET_STARTUP_HOOKS. + // Determine the process specification to use based on whether we're running on macOS/Linux or Windows. ProcessSpecification processSpecification; if (OperatingSystem.IsWindows()) diff --git a/UET/Redpoint.Uet.Uat/Redpoint.Uet.Uat.csproj b/UET/Redpoint.Uet.Uat/Redpoint.Uet.Uat.csproj index 80c96db9..5daf0418 100644 --- a/UET/Redpoint.Uet.Uat/Redpoint.Uet.Uat.csproj +++ b/UET/Redpoint.Uet.Uat/Redpoint.Uet.Uat.csproj @@ -12,6 +12,13 @@ + + + + + + Embedded\%(Filename)%(Extension) + diff --git a/UET/UET.sln b/UET/UET.sln index b0206e30..3eeae0c6 100644 --- a/UET/UET.sln +++ b/UET/UET.sln @@ -315,6 +315,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Redpoint.Uba", "Redpoint.Ub EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Redpoint.Uba", "Redpoint.Uba\Redpoint.Uba.csproj", "{2AAD5971-F06A-4B7A-9E11-538BEC82F247}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Redpoint.Uet.Patching.Runtime", "Redpoint.Uet.Patching.Runtime\Redpoint.Uet.Patching.Runtime.csproj", "{2B61E65D-9CCD-4671-B713-BD3CD4694014}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -857,6 +859,10 @@ Global {2AAD5971-F06A-4B7A-9E11-538BEC82F247}.Debug|Any CPU.Build.0 = Debug|Any CPU {2AAD5971-F06A-4B7A-9E11-538BEC82F247}.Release|Any CPU.ActiveCfg = Release|Any CPU {2AAD5971-F06A-4B7A-9E11-538BEC82F247}.Release|Any CPU.Build.0 = Release|Any CPU + {2B61E65D-9CCD-4671-B713-BD3CD4694014}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2B61E65D-9CCD-4671-B713-BD3CD4694014}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2B61E65D-9CCD-4671-B713-BD3CD4694014}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2B61E65D-9CCD-4671-B713-BD3CD4694014}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -967,6 +973,7 @@ Global {7CDD0F44-C516-47E9-99E5-3885AD24636F} = {A10A6C63-109E-4825-AA79-81B0AE279A76} {D19B5289-C0A3-4025-834D-FA8AB1C19A32} = {1AE4AFCD-0F49-4CEA-8439-F1AAA2CDD183} {2AAD5971-F06A-4B7A-9E11-538BEC82F247} = {586E33AB-CC82-4ADF-92F3-3B287A5AEEAC} + {2B61E65D-9CCD-4671-B713-BD3CD4694014} = {1AE4AFCD-0F49-4CEA-8439-F1AAA2CDD183} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {8598A278-509A-48A6-A7B3-3E3B0D1011F1}