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}