diff --git a/BUILDING.md b/BUILDING.md index 3afcad4..5da714f 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -14,7 +14,7 @@ Clone this repository via `git clone https://github.com/BepInEx/BepInEx.git`. After that, run in the repository directory ```bat -build.bat -target=Build +build.bat --target=Build ``` ### Windows (PowerShell) @@ -23,7 +23,7 @@ Clone this repository via `git clone https://github.com/BepInEx/BepInEx.git`. After that, run in the repository directory ```ps -./build.ps1 -target=Build +./build.ps1 --target=Build ``` Make sure you have the execution policy set to enable running scripts. diff --git a/BepInEx.Preloader/BepInEx.Preloader.csproj b/BepInEx.Preloader/BepInEx.Preloader.csproj index f054cf3..ae84455 100644 --- a/BepInEx.Preloader/BepInEx.Preloader.csproj +++ b/BepInEx.Preloader/BepInEx.Preloader.csproj @@ -15,10 +15,10 @@ - + - - + + @@ -27,4 +27,4 @@ - \ No newline at end of file + diff --git a/BepInEx.Preloader/Entrypoint.cs b/BepInEx.Preloader/Entrypoint.cs index b9438cb..ea1d931 100644 --- a/BepInEx.Preloader/Entrypoint.cs +++ b/BepInEx.Preloader/Entrypoint.cs @@ -45,8 +45,6 @@ private static void LoadCriticalAssemblies() public static void PreloaderPreMain() { - PlatformUtils.SetPlatform(); - string bepinPath = Utility.ParentDirectory(Path.GetFullPath(EnvVars.DOORSTOP_INVOKE_DLL_PATH), 2); Paths.SetExecutablePath(EnvVars.DOORSTOP_PROCESS_PATH, bepinPath, EnvVars.DOORSTOP_MANAGED_FOLDER_DIR, EnvVars.DOORSTOP_DLL_SEARCH_DIRS); @@ -92,8 +90,8 @@ private static Assembly LocalResolve(object sender, ResolveEventArgs args) return foundAssembly; if (Utility.TryResolveDllAssembly(assemblyName, Paths.BepInExAssemblyDirectory, out foundAssembly) - || Utility.TryResolveDllAssembly(assemblyName, Paths.PatcherPluginPath, out foundAssembly) - || Utility.TryResolveDllAssembly(assemblyName, Paths.PluginPath, out foundAssembly)) + || (Directory.Exists(Paths.PatcherPluginPath) && Utility.TryResolveDllAssembly(assemblyName, Paths.PatcherPluginPath, out foundAssembly)) + || (Directory.Exists(Paths.PluginPath) && Utility.TryResolveDllAssembly(assemblyName, Paths.PluginPath, out foundAssembly))) return foundAssembly; return null; @@ -158,4 +156,4 @@ internal static Assembly ResolveCurrentDirectory(object sender, ResolveEventArgs } } } -} \ No newline at end of file +} diff --git a/BepInEx.Preloader/Platform.cs b/BepInEx.Preloader/Platform.cs deleted file mode 100644 index 3c73362..0000000 --- a/BepInEx.Preloader/Platform.cs +++ /dev/null @@ -1,151 +0,0 @@ -using System; -using System.IO; -using System.Reflection; -using System.Runtime.InteropServices; -using MonoMod.Utils; - -namespace BepInEx.Preloader -{ - internal static class PlatformUtils - { - [StructLayout(LayoutKind.Sequential, Pack = 1)] - public struct utsname_osx - { - private const int osx_utslen = 256; - - [MarshalAs(UnmanagedType.ByValTStr, SizeConst = osx_utslen)] - public string sysname; - - [MarshalAs(UnmanagedType.ByValTStr, SizeConst = osx_utslen)] - public string nodename; - - [MarshalAs(UnmanagedType.ByValTStr, SizeConst = osx_utslen)] - public string release; - - [MarshalAs(UnmanagedType.ByValTStr, SizeConst = osx_utslen)] - public string version; - - [MarshalAs(UnmanagedType.ByValTStr, SizeConst = osx_utslen)] - public string machine; - } - - [StructLayout(LayoutKind.Sequential, Pack = 1)] - public struct utsname_linux - { - private const int linux_utslen = 65; - - [MarshalAs(UnmanagedType.ByValTStr, SizeConst = linux_utslen)] - public string sysname; - - [MarshalAs(UnmanagedType.ByValTStr, SizeConst = linux_utslen)] - public string nodename; - - [MarshalAs(UnmanagedType.ByValTStr, SizeConst = linux_utslen)] - public string release; - - [MarshalAs(UnmanagedType.ByValTStr, SizeConst = linux_utslen)] - public string version; - - [MarshalAs(UnmanagedType.ByValTStr, SizeConst = linux_utslen)] - public string machine; - - [MarshalAs(UnmanagedType.ByValTStr, SizeConst = linux_utslen)] - public string domainname; - } - - [DllImport("libc.so.6", EntryPoint = "uname", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] - private static extern IntPtr uname_linux(ref utsname_linux utsname); - - [DllImport("/usr/lib/libSystem.dylib", EntryPoint = "uname", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] - private static extern IntPtr uname_osx(ref utsname_osx utsname); - - /// - /// Recreation of MonoMod's PlatformHelper.DeterminePlatform method, but with libc calls instead of creating processes. - /// - public static void SetPlatform() - { - var current = Platform.Unknown; - - // For old Mono, get from a private property to accurately get the platform. - // static extern PlatformID Platform - PropertyInfo p_Platform = typeof(Environment).GetProperty("Platform", BindingFlags.NonPublic | BindingFlags.Static); - string platID; - if (p_Platform != null) - { - platID = p_Platform.GetValue(null, new object[0]).ToString(); - } - else - { - // For .NET and newer Mono, use the usual value. - platID = Environment.OSVersion.Platform.ToString(); - } - platID = platID.ToLowerInvariant(); - - if (platID.Contains("win")) - { - current = Platform.Windows; - } - else if (platID.Contains("mac") || platID.Contains("osx")) - { - current = Platform.MacOS; - } - else if (platID.Contains("lin") || platID.Contains("unix")) - { - current = Platform.Linux; - } - - if (Is(current, Platform.Linux) && Directory.Exists("/data") && File.Exists("/system/build.prop")) - { - current = Platform.Android; - } - else if (Is(current, Platform.Unix) && Directory.Exists("/System/Library/AccessibilityBundles")) - { - current = Platform.iOS; - } - - // Is64BitOperatingSystem has been added in .NET Framework 4.0 - MethodInfo m_get_Is64BitOperatingSystem = typeof(Environment).GetProperty("Is64BitOperatingSystem")?.GetGetMethod(); - if (m_get_Is64BitOperatingSystem != null) - current |= (((bool)m_get_Is64BitOperatingSystem.Invoke(null, new object[0])) ? Platform.Bits64 : 0); - else - current |= (IntPtr.Size >= 8 ? Platform.Bits64 : 0); - - if ((Is(current, Platform.MacOS) || Is(current, Platform.Linux)) && Type.GetType("Mono.Runtime") != null) - { - string arch; - IntPtr result; - - if (Is(current, Platform.MacOS)) - { - utsname_osx utsname_osx = new utsname_osx(); - result = uname_osx(ref utsname_osx); - arch = utsname_osx.machine; - } - else - { - // Linux - utsname_linux utsname_linux = new utsname_linux(); - result = uname_linux(ref utsname_linux); - arch = utsname_linux.machine; - } - - if (result == IntPtr.Zero && (arch.StartsWith("aarch") || arch.StartsWith("arm"))) - current |= Platform.ARM; - } - else - { - // Detect ARM based on PE info or uname. - typeof(object).Module.GetPEKind(out PortableExecutableKinds peKind, out ImageFileMachine machine); - if (machine == (ImageFileMachine)0x01C4 /* ARM, .NET Framework 4.5 */) - current |= Platform.ARM; - } - - PlatformHelper.Current = current; - } - - private static bool Is(Platform current, Platform expected) - { - return (current & expected) == expected; - } - } -} \ No newline at end of file diff --git a/BepInEx.Preloader/Preloader.cs b/BepInEx.Preloader/Preloader.cs index b57ba42..95b28df 100644 --- a/BepInEx.Preloader/Preloader.cs +++ b/BepInEx.Preloader/Preloader.cs @@ -73,7 +73,7 @@ public static void Run() Logger.LogInfo($"Running under Unity v{GetUnityVersion()}"); Logger.LogInfo($"CLR runtime version: {Environment.Version}"); Logger.LogInfo($"Supports SRE: {Utility.CLRSupportsDynamicAssemblies}"); - Logger.LogInfo($"System platform: {PlatformHelper.Current}"); + Logger.LogInfo($"System platform: {PlatformDetection.OS} {PlatformDetection.Architecture}"); if (runtimePatchException != null) Logger.LogWarning($"Failed to apply runtime patches for Mono. See more info in the output log. Error message: {runtimePatchException.Message}"); @@ -250,7 +250,7 @@ public static void AllocateConsole() public static string GetUnityVersion() { - if (PlatformHelper.Is(Platform.Windows)) + if (PlatformDetection.OS.Is(OSKind.Windows)) return FileVersionInfo.GetVersionInfo(Paths.ExecutablePath).FileVersion; return $"Unknown ({(IsPostUnity2017 ? "post" : "pre")}-2017)"; @@ -318,4 +318,4 @@ private enum MonoModBackend #endregion } -} \ No newline at end of file +} diff --git a/BepInEx.Preloader/RuntimeFixes/XTermFix.cs b/BepInEx.Preloader/RuntimeFixes/XTermFix.cs index 7db6ef5..bf330e6 100644 --- a/BepInEx.Preloader/RuntimeFixes/XTermFix.cs +++ b/BepInEx.Preloader/RuntimeFixes/XTermFix.cs @@ -3,8 +3,6 @@ using System.Linq; using System.Reflection.Emit; using HarmonyLib; -using MonoMod.RuntimeDetour; -using MonoMod.RuntimeDetour.Platforms; using MonoMod.Utils; namespace BepInEx.Preloader.RuntimeFixes @@ -13,7 +11,7 @@ internal static class XTermFix { public static void Apply() { - if (PlatformHelper.Is(Platform.Windows)) + if (PlatformDetection.OS.Is(OSKind.Windows)) return; if (typeof(Console).Assembly.GetType("System.ConsoleDriver") == null) @@ -28,13 +26,6 @@ public static void Apply() return; } - // Apparently on older Unity versions (4.x), using Process.Start can run Console..cctor - // And since MonoMod's PlatformHelper (used by DetourHelper.Native) runs Process.Start to determine ARM/x86 platform, - // this causes a crash owing to TermInfoReader running before it can be patched and fixed - // Because Doorstop does not support ARM at the moment, we can get away with just forcing x86 detour platform. - // TODO: Figure out a way to detect ARM on Unix without running Process.Start - DetourHelper.Native = new DetourNativeX86Platform(); - var harmony = new HarmonyLib.Harmony("com.bepinex.xtermfix"); harmony.Patch(AccessTools.Method("System.TermInfoReader:ReadHeader"), @@ -48,8 +39,6 @@ public static void Apply() harmony.Patch(AccessTools.Method("System.TermInfoReader:GetStringBytes", new []{ AccessTools.TypeByName("System.TermInfoStrings") }), transpiler: new HarmonyMethod(typeof(XTermFix), nameof(GetTermInfoStringsTranspiler))); - - DetourHelper.Native = null; } public static int intOffset; @@ -133,4 +122,4 @@ public static IEnumerable GetTermInfoStringsTranspiler(IEnumera return list; } } -} \ No newline at end of file +} diff --git a/BepInEx/BepInEx.csproj b/BepInEx/BepInEx.csproj index 3451f39..7de0c6f 100644 --- a/BepInEx/BepInEx.csproj +++ b/BepInEx/BepInEx.csproj @@ -27,10 +27,10 @@ - + - - + + @@ -40,4 +40,4 @@ - \ No newline at end of file + diff --git a/BepInEx/Bootstrap/Chainloader.cs b/BepInEx/Bootstrap/Chainloader.cs index 088ae89..91ed2ef 100644 --- a/BepInEx/Bootstrap/Chainloader.cs +++ b/BepInEx/Bootstrap/Chainloader.cs @@ -48,7 +48,13 @@ private static bool IsHeadless var prop = AccessTools.PropertyGetter(typeof(Application), "isBatchMode"); if (prop != null) return (bool) prop.Invoke(null, null); - return SystemInfo.graphicsDeviceID == 0; + // Some Unity builds may miss this property, see + // https://docs.unity3d.com/2022.3/Documentation/ScriptReference/SystemInfo-graphicsDeviceID.html + var prop2 = AccessTools.PropertyGetter(typeof(SystemInfo), "graphicsDeviceID"); + if (prop2 != null) + return ((int) prop2.Invoke(null, null)) == 0; + + return false; } } @@ -140,7 +146,7 @@ public static void Initialize(string gameExePath, bool startConsole = true, ICol consoleLogListener.WriteUnityLogs = false; } - if (PlatformHelper.Is(Platform.Unix)) + if (PlatformDetection.OS.Is(OSKind.Posix)) { Logger.LogInfo($"Detected Unity version: v{UnityVersion}"); } diff --git a/BepInEx/Console/ConsoleManager.cs b/BepInEx/Console/ConsoleManager.cs index 337d6d0..47fd5a0 100644 --- a/BepInEx/Console/ConsoleManager.cs +++ b/BepInEx/Console/ConsoleManager.cs @@ -54,9 +54,9 @@ static ConsoleManager() public static void Initialize(bool alreadyActive) { - if (PlatformHelper.Is(Platform.Unix)) + if (PlatformDetection.OS.Is(OSKind.Posix)) Driver = new LinuxConsoleDriver(); - else if (PlatformHelper.Is(Platform.Windows)) + else if (PlatformDetection.OS.Is(OSKind.Windows)) Driver = new WindowsConsoleDriver(); Driver.Initialize(alreadyActive); diff --git a/BepInEx/Console/Unix/UnixStreamHelper.cs b/BepInEx/Console/Unix/UnixStreamHelper.cs index c82e5bb..71ccb4b 100644 --- a/BepInEx/Console/Unix/UnixStreamHelper.cs +++ b/BepInEx/Console/Unix/UnixStreamHelper.cs @@ -1,55 +1,50 @@ using System; -using System.Collections.Generic; using System.IO; +using System.Runtime.InteropServices; using MonoMod.Utils; namespace BepInEx.Unix { internal static class UnixStreamHelper { + private static IntPtr libcHandle; + public delegate int dupDelegate(int fd); - [DynDllImport("libc")] public static dupDelegate dup; public delegate IntPtr fdopenDelegate(int fd, string mode); - [DynDllImport("libc")] public static fdopenDelegate fdopen; public delegate IntPtr freadDelegate(IntPtr ptr, IntPtr size, IntPtr nmemb, IntPtr stream); - [DynDllImport("libc")] public static freadDelegate fread; public delegate int fwriteDelegate(IntPtr ptr, IntPtr size, IntPtr nmemb, IntPtr stream); - [DynDllImport("libc")] public static fwriteDelegate fwrite; public delegate int fcloseDelegate(IntPtr stream); - [DynDllImport("libc")] public static fcloseDelegate fclose; public delegate int fflushDelegate(IntPtr stream); - [DynDllImport("libc")] public static fflushDelegate fflush; public delegate int isattyDelegate(int fd); - [DynDllImport("libc")] public static isattyDelegate isatty; static UnixStreamHelper() { - var libcMapping = new Dictionary> - { - ["libc"] = new List - { - "libc.so.6", // Ubuntu glibc - "libc", // Linux glibc - "/usr/lib/libSystem.dylib", // OSX POSIX - } - }; - - typeof(UnixStreamHelper).ResolveDynDllImports(libcMapping); + libcHandle = DynDll.OpenLibrary(PlatformDetection.OS.Is(OSKind.OSX) ? "/usr/lib/libSystem.dylib" : "libc"); + dup = AsDelegate(libcHandle.GetExport("dup")); + fdopen = AsDelegate(libcHandle.GetExport("fdopen")); + fread = AsDelegate(libcHandle.GetExport("fread")); + fwrite = AsDelegate(libcHandle.GetExport("fwrite")); + fclose = AsDelegate(libcHandle.GetExport("fclose")); + fflush = AsDelegate(libcHandle.GetExport("fflush")); + isatty = AsDelegate(libcHandle.GetExport("isatty")); } + private static T AsDelegate(IntPtr symbol) where T : class + => Marshal.GetDelegateForFunctionPointer(symbol, typeof(T)) as T; + public static Stream CreateDuplicateStream(int fileDescriptor) { int newFd = dup(fileDescriptor); @@ -57,4 +52,4 @@ public static Stream CreateDuplicateStream(int fileDescriptor) return new UnixStream(newFd, FileAccess.Write); } } -} \ No newline at end of file +} diff --git a/BepInEx/Console/Windows/ConsoleWindow.cs b/BepInEx/Console/Windows/ConsoleWindow.cs index f325844..3430192 100644 --- a/BepInEx/Console/Windows/ConsoleWindow.cs +++ b/BepInEx/Console/Windows/ConsoleWindow.cs @@ -118,11 +118,14 @@ private static void Initialize() // Some games may ship user32.dll with some methods missing. As such, we load the DLL explicitly from system folder var user32Dll = LoadLibraryEx("user32.dll", IntPtr.Zero, LOAD_LIBRARY_SEARCH_SYSTEM32); - setForeground = GetProcAddress(user32Dll, "SetForegroundWindow").AsDelegate(); - getForeground = GetProcAddress(user32Dll,"GetForegroundWindow").AsDelegate(); - getSystemMenu = GetProcAddress(user32Dll,"GetSystemMenu").AsDelegate(); - deleteMenu = GetProcAddress(user32Dll,"DeleteMenu").AsDelegate(); + setForeground = AsDelegate(GetProcAddress(user32Dll, "SetForegroundWindow")); + getForeground = AsDelegate(GetProcAddress(user32Dll, "GetForegroundWindow")); + getSystemMenu = AsDelegate(GetProcAddress(user32Dll, "GetSystemMenu")); + deleteMenu = AsDelegate(GetProcAddress(user32Dll, "DeleteMenu")); } + + private static T AsDelegate(IntPtr symbol) where T : class + => Marshal.GetDelegateForFunctionPointer(symbol, typeof(T)) as T; [DllImport("kernel32.dll", SetLastError=true)] static extern IntPtr GetProcAddress(IntPtr hModule, string procName); diff --git a/BepInEx/Logging/UnityLogListener.cs b/BepInEx/Logging/UnityLogListener.cs index 8ff8ea0..67d2982 100644 --- a/BepInEx/Logging/UnityLogListener.cs +++ b/BepInEx/Logging/UnityLogListener.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Reflection; using System.Runtime.CompilerServices; using System.Text; @@ -79,6 +79,22 @@ static UnityLogListener() } } + // Fix for Unity 6. Both methods previously targeted no longer use internal calls + if (WriteStringToUnityLog == null) + { + try + { + var type = Type.GetType("UnityEngine.UnityLogWriter, UnityEngine.CoreModule"); + var methodInfo = type.GetMethod("WriteStringToUnityLogImpl", + BindingFlags.Static | BindingFlags.NonPublic, + null, + new Type[] { typeof(string) }, + null); + methodInfo.Invoke(null, new object[] { "" }); + WriteStringToUnityLog = (Action)Delegate.CreateDelegate(typeof(Action), methodInfo); + } catch { } + } + if (WriteStringToUnityLog == null) Logger.LogError("Unable to start Unity log writer"); } diff --git a/BepInEx/Paths.cs b/BepInEx/Paths.cs index 1e49e41..ffad90f 100644 --- a/BepInEx/Paths.cs +++ b/BepInEx/Paths.cs @@ -15,7 +15,7 @@ internal static void SetExecutablePath(string executablePath, string bepinRootPa ExecutablePath = executablePath; ProcessName = Path.GetFileNameWithoutExtension(executablePath); - GameRootPath = PlatformHelper.Is(Platform.MacOS) + GameRootPath = PlatformDetection.OS.Is(OSKind.OSX) ? Utility.ParentDirectory(executablePath, 4) : Path.GetDirectoryName(executablePath); @@ -112,4 +112,4 @@ internal static void SetPluginPath(string pluginPath) /// public static string ProcessName { get; private set; } } -} \ No newline at end of file +} diff --git a/BepInExTests/BepInExTests.csproj b/BepInExTests/BepInExTests.csproj index 0233b79..b3f5c8b 100644 --- a/BepInExTests/BepInExTests.csproj +++ b/BepInExTests/BepInExTests.csproj @@ -1,6 +1,6 @@  - net45 + net462 @@ -12,6 +12,7 @@ + @@ -33,4 +34,4 @@ False - \ No newline at end of file + diff --git a/Directory.Build.props b/Directory.Build.props index a6645e4..8596ee8 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,5 +1,5 @@ - 5.4.23.3 + 5.4.23.5 diff --git a/README.md b/README.md index ba03a7d..613a93b 100644 --- a/README.md +++ b/README.md @@ -22,8 +22,8 @@ Unity plugin framework **[User and developer guides](https://bepinex.github.io/bepinex_docs/master/articles/index.html)** ## Used libraries -- [NeighTools/UnityDoorstop](https://github.com/NeighTools/UnityDoorstop) - 4.4.0 ([851712b](https://github.com/NeighTools/UnityDoorstop/commit/851712bfef21c90e01c951e024609e76716d55f9)) -- [BepInEx/HarmonyX](https://github.com/BepInEx/HarmonyX) - 2.7.0 ([2537257](https://github.com/BepInEx/HarmonyX/commit/253725768e59b0e1ea90105cdbcc4a0a477422c7)) +- [NeighTools/UnityDoorstop](https://github.com/NeighTools/UnityDoorstop) - 4.5.0 ([33dab9a](https://github.com/NeighTools/UnityDoorstop/commit/33dab9a6733862eb81869ff08431d9478b28784b)) +- [BepInEx/HarmonyX](https://github.com/BepInEx/HarmonyX) - 2.9.0 ([31d794f](https://github.com/BepInEx/HarmonyX/commit/31d794f3affce55fa87c99efac7dae23a126cf52)) - [MonoMod/MonoMod](https://github.com/MonoMod/MonoMod) - v21.12.13.01 ([ede81f4](https://github.com/MonoMod/MonoMod/commit/ede81f48924d58abf05359409fad740fe2b0dfb5)) - [jbevain/cecil](https://github.com/jbevain/cecil) - 0.10.4 ([98ec890](https://github.com/jbevain/cecil/commit/98ec890d44643ad88d573e97be0e120435eda732)) diff --git a/TestPlugins/ValheimArm64Smoke.Harmony/Plugin.cs b/TestPlugins/ValheimArm64Smoke.Harmony/Plugin.cs new file mode 100644 index 0000000..9d9ea04 --- /dev/null +++ b/TestPlugins/ValheimArm64Smoke.Harmony/Plugin.cs @@ -0,0 +1,104 @@ +using System; +using System.IO; +using System.Reflection; +using BepInEx; +using HarmonyLib; + +namespace ValheimArm64Smoke.Harmony +{ + [BepInPlugin(PluginGuid, PluginName, PluginVersion)] + public sealed class Plugin : BaseUnityPlugin + { + private const string PluginGuid = "dev.bepinex.valheimarm64.harmony"; + private const string PluginName = "Valheim ARM64 Smoke Test (Harmony)"; + private const string PluginVersion = "0.1.0"; + + private static readonly string[] TargetCandidates = + { + "FejdStartup:Awake", + "FejdStartup:Start", + "Game:Awake", + "ZNet:Awake" + }; + + private static BepInEx.Logging.ManualLogSource log; + private static string markerPath; + private HarmonyLib.Harmony harmony; + private static bool prefixLogged; + + private void Awake() + { + log = Logger; + markerPath = Path.Combine(Paths.BepInExRootPath, "valheim-arm64-harmony.txt"); + + try + { + var target = ResolveTargetMethod(); + if (target == null) + { + WriteMarker("status=target_not_found"); + Logger.LogWarning($"{PluginName} could not resolve a Valheim target method"); + return; + } + + harmony = new HarmonyLib.Harmony(PluginGuid); + var prefix = typeof(Plugin).GetMethod(nameof(TargetPrefix), BindingFlags.NonPublic | BindingFlags.Static); + + harmony.Patch(target, prefix: new HarmonyMethod(prefix)); + WriteMarker( + "status=patched", + $"target={target.DeclaringType.FullName}.{target.Name}", + $"module={target.Module.Name}"); + Logger.LogInfo($"{PluginName} patched {target.DeclaringType.FullName}.{target.Name}"); + } + catch (Exception ex) + { + WriteMarker("status=patch_failed", ex.ToString()); + Logger.LogError($"{PluginName} failed while patching: {ex}"); + } + } + + private static MethodBase ResolveTargetMethod() + { + foreach (var candidate in TargetCandidates) + { + var splitIndex = candidate.IndexOf(':'); + if (splitIndex <= 0 || splitIndex >= candidate.Length - 1) + continue; + + var typeName = candidate.Substring(0, splitIndex); + var methodName = candidate.Substring(splitIndex + 1); + var type = AccessTools.TypeByName(typeName); + var method = type != null ? AccessTools.DeclaredMethod(type, methodName) : null; + + if (method != null) + return method; + } + + return null; + } + + private static void TargetPrefix(MethodBase __originalMethod) + { + if (prefixLogged) + return; + + prefixLogged = true; + var originalName = $"{__originalMethod.DeclaringType.FullName}.{__originalMethod.Name}"; + WriteMarker("status=prefix_hit", $"method={originalName}", $"timestamp_utc={DateTime.UtcNow:O}"); + log?.LogInfo($"{PluginName} prefix executed for {originalName}"); + } + + private static void WriteMarker(params string[] lines) + { + try + { + File.WriteAllLines(markerPath, lines); + } + catch (Exception ex) + { + log?.LogWarning($"Could not write smoke marker {markerPath}: {ex}"); + } + } + } +} diff --git a/TestPlugins/ValheimArm64Smoke.Harmony/ValheimArm64Smoke.Harmony.csproj b/TestPlugins/ValheimArm64Smoke.Harmony/ValheimArm64Smoke.Harmony.csproj new file mode 100644 index 0000000..fe18172 --- /dev/null +++ b/TestPlugins/ValheimArm64Smoke.Harmony/ValheimArm64Smoke.Harmony.csproj @@ -0,0 +1,16 @@ + + + net35 + 8 + ValheimArm64Smoke.Harmony + ValheimArm64Smoke.Harmony + false + false + + + + + + + + diff --git a/TestPlugins/ValheimArm64Smoke.NoHarmony/Plugin.cs b/TestPlugins/ValheimArm64Smoke.NoHarmony/Plugin.cs new file mode 100644 index 0000000..ed880c0 --- /dev/null +++ b/TestPlugins/ValheimArm64Smoke.NoHarmony/Plugin.cs @@ -0,0 +1,32 @@ +using System; +using System.IO; +using BepInEx; +using UnityEngine; + +namespace ValheimArm64Smoke.NoHarmony +{ + [BepInPlugin(PluginGuid, PluginName, PluginVersion)] + public sealed class Plugin : BaseUnityPlugin + { + private const string PluginGuid = "dev.bepinex.valheimarm64.noharmony"; + private const string PluginName = "Valheim ARM64 Smoke Test (No Harmony)"; + private const string PluginVersion = "0.1.0"; + + private void Awake() + { + var markerPath = Path.Combine(Paths.BepInExRootPath, "valheim-arm64-noharmony.txt"); + var markerLines = new[] + { + $"timestamp_utc={DateTime.UtcNow:O}", + $"unity_version={Application.unityVersion}", + $"unity_platform={Application.platform}", + $"pointer_size_bits={IntPtr.Size * 8}", + $"bepinex_version={typeof(BaseUnityPlugin).Assembly.GetName().Version}" + }; + + File.WriteAllLines(markerPath, markerLines); + Logger.LogInfo($"{PluginName} loaded successfully"); + Logger.LogInfo($"Wrote smoke marker to {markerPath}"); + } + } +} diff --git a/TestPlugins/ValheimArm64Smoke.NoHarmony/ValheimArm64Smoke.NoHarmony.csproj b/TestPlugins/ValheimArm64Smoke.NoHarmony/ValheimArm64Smoke.NoHarmony.csproj new file mode 100644 index 0000000..a9cdee7 --- /dev/null +++ b/TestPlugins/ValheimArm64Smoke.NoHarmony/ValheimArm64Smoke.NoHarmony.csproj @@ -0,0 +1,15 @@ + + + net35 + 8 + ValheimArm64Smoke.NoHarmony + ValheimArm64Smoke.NoHarmony + false + false + + + + + + + diff --git a/build.cake b/build.cake index fa287d7..1b48e67 100644 --- a/build.cake +++ b/build.cake @@ -4,7 +4,8 @@ #addin nuget:?package=Cake.Json&version=7.0.1 #addin nuget:?package=Newtonsoft.Json&version=13.0.3 -const string DOORSTOP_VER = "4.4.0"; +const string DOORSTOP_VER = "4.5.0"; +var customMacDoorstopDir = EnvironmentVariable("BEPINEX_MACOS_DOORSTOP_DIR"); var target = Argument("target", "Build"); var isBleedingEdge = Argument("bleeding_edge", false); @@ -104,16 +105,29 @@ Task("DownloadDoorstop") var doorstopWinPath = doorstopPath + File("doorstop_win.zip"); var doorstopLinuxPath = doorstopPath + File("doorstop_linux.zip"); var doorstopMacPath = doorstopPath + File("doorstop_macos.zip"); + var doorstopMacOutputPath = doorstopPath + Directory("macos") + Directory("universal"); CreateDirectory(doorstopPath); + CreateDirectory(doorstopMacOutputPath); DownloadFile($"https://github.com/NeighTools/UnityDoorstop/releases/download/v{DOORSTOP_VER}/doorstop_win_release_{DOORSTOP_VER}.zip", doorstopWinPath); DownloadFile($"https://github.com/NeighTools/UnityDoorstop/releases/download/v{DOORSTOP_VER}/doorstop_linux_release_{DOORSTOP_VER}.zip", doorstopLinuxPath); - DownloadFile($"https://github.com/NeighTools/UnityDoorstop/releases/download/v{DOORSTOP_VER}/doorstop_macos_release_{DOORSTOP_VER}.zip", doorstopMacPath); + + if (!string.IsNullOrWhiteSpace(customMacDoorstopDir)) + { + Information($"Using custom macOS Doorstop from {customMacDoorstopDir}"); + CopyFileToDirectory(File(System.IO.Path.Combine(customMacDoorstopDir, "libdoorstop.dylib")), doorstopMacOutputPath); + CopyFileToDirectory(File(System.IO.Path.Combine(customMacDoorstopDir, ".doorstop_version")), doorstopMacOutputPath); + } + else + { + DownloadFile($"https://github.com/NeighTools/UnityDoorstop/releases/download/v{DOORSTOP_VER}/doorstop_macos_release_{DOORSTOP_VER}.zip", doorstopMacPath); + } Information("Extracting Doorstop"); ZipUncompress(doorstopWinPath, doorstopPath + Directory("win")); ZipUncompress(doorstopLinuxPath, doorstopPath + Directory("linux")); - ZipUncompress(doorstopMacPath, doorstopPath + Directory("macos")); + if (string.IsNullOrWhiteSpace(customMacDoorstopDir)) + ZipUncompress(doorstopMacPath, doorstopPath + Directory("macos")); }); Task("MakeDist") @@ -163,7 +177,7 @@ Task("MakeDist") PackageBepin("win", "x86", "winhttp.dll", "doorstop_config.ini"); PackageBepin("linux", "x64", "libdoorstop.so", "run_bepinex.sh", true); PackageBepin("linux", "x86", "libdoorstop.so", "run_bepinex.sh", true); - PackageBepin("macos", "x64", "libdoorstop.dylib", "run_bepinex.sh", true); + PackageBepin("macos", "universal", "libdoorstop.dylib", "run_bepinex.sh", true); CopyFileToDirectory(File("./bin/patcher/BepInEx.Patcher.exe"), distPatcherDir); }); @@ -179,7 +193,7 @@ Task("Pack") ZipCompress(distDir + Directory("win_x64"), distDir + File($"BepInEx_win_x64{commitPrefix}{buildVersion}.zip")); ZipCompress(distDir + Directory("linux_x86"), distDir + File($"BepInEx_linux_x86{commitPrefix}{buildVersion}.zip")); ZipCompress(distDir + Directory("linux_x64"), distDir + File($"BepInEx_linux_x64{commitPrefix}{buildVersion}.zip")); - ZipCompress(distDir + Directory("macos_x64"), distDir + File($"BepInEx_macos_x64{commitPrefix}{buildVersion}.zip")); + ZipCompress(distDir + Directory("macos_universal"), distDir + File($"BepInEx_macos_universal{commitPrefix}{buildVersion}.zip")); Information("Packing BepInEx.Patcher"); ZipCompress(distDir + Directory("patcher"), distDir + File($"BepInEx_Patcher{commitPrefix}{buildVersion}.zip")); @@ -222,4 +236,4 @@ Task("Pack") } }); -RunTarget(target); \ No newline at end of file +RunTarget(target); diff --git a/build.sh b/build.sh old mode 100644 new mode 100755 diff --git a/docs/valheim-macos-arm64.md b/docs/valheim-macos-arm64.md new file mode 100644 index 0000000..f3978af --- /dev/null +++ b/docs/valheim-macos-arm64.md @@ -0,0 +1,73 @@ +# Valheim on macOS Apple Silicon + +Tested on April 6, 2026 on an Apple M4 Pro MacBook Pro with the Steam build of Valheim at: + +`/Users/$USER/Library/Application Support/Steam/steamapps/common/Valheim/valheim.app` + +## What was validated + +- Valheim's macOS executable is a universal binary with `x86_64` and `arm64` slices. +- BepInEx `5.4.23.5` starts natively on macOS Apple Silicon with this branch. +- HarmonyX `2.16.1` plus MonoMod `25.x` is working for simple Harmony patches on this setup. +- The following real Valheim mods loaded successfully in native Apple Silicon testing: + - `ValheimModding-Jotunn-2.28.0` + - `Azumatt-AzuCraftyBoxes-1.8.13` + +## The important blocker on newer Apple Silicon + +The official UnityDoorstop `4.5.0` macOS release ships `libdoorstop.dylib` as `x86_64, arm64`. + +On this M4 Pro test machine, `dyld` rejected that binary before managed code even started: + +`missing compatible architecture (have 'x86_64,arm64', need 'arm64e')` + +For this machine, the minimal working fix was to rebuild UnityDoorstop so the macOS universal dylib contains `x86_64, arm64e`. + +## Install from this branch + +Build and install into the default Steam Valheim app bundle: + +```bash +scripts/valheim/install_macos_arm64.sh --with-smoke-tests +``` + +Install directly from a Gale or r2modman profile export: + +```bash +scripts/valheim/install_gale_export.sh --export ~/Downloads/Servidor.r2z +``` + +Install into a custom Valheim path: + +```bash +scripts/valheim/install_macos_arm64.sh "/path/to/valheim.app" +``` + +Copy an exported Windows or Gale plugin directory into the install at the same time: + +```bash +scripts/valheim/install_macos_arm64.sh --mods-dir "/path/to/BepInEx/plugins" +``` + +Run the game: + +```bash +cd "/Users/$USER/Library/Application Support/Steam/steamapps/common/Valheim/valheim.app/Contents/MacOS" +./run_bepinex.sh ./Valheim +``` + +## Smoke tests in this repo + +Two test plugins are included under `TestPlugins/`: + +- `ValheimArm64Smoke.NoHarmony` +- `ValheimArm64Smoke.Harmony` + +The Harmony smoke test successfully patched and hit `FejdStartup.Awake` during native startup on the M4 Pro validation run. + +## Remaining risks + +- Mods with extra native libraries may still need macOS-specific binaries. +- Heavy IL manipulation and less common detour paths still need broader coverage than the smoke test and the two real mods above. +- If a mod pack was exported from Windows, only copy `BepInEx/plugins`, `BepInEx/config`, and `BepInEx/patchers`. Do not copy Windows root files like `winhttp.dll`. +- Gale `.r2z` exports do not include every mod DLL directly; this repo's installer resolves the package list from `export.r2x` and downloads the exact versions from Thunderstore. diff --git a/doorstop/run_bepinex.sh b/doorstop/run_bepinex.sh index 798c3af..d27b658 100644 --- a/doorstop/run_bepinex.sh +++ b/doorstop/run_bepinex.sh @@ -8,8 +8,6 @@ # 1. Via CLI: Run ./run_bepinex.sh [doorstop arguments] [game arguments] # 2. Via config: edit the options below and run ./run.sh without any arguments -# 0 is false, 1 is true - # LINUX: name of Unity executable # MACOS: name of the .app directory executable_name="" @@ -19,6 +17,7 @@ executable_name="" # General Config Options # Enable Doorstop? +# 0 is false, 1 is true enabled="1" # Path to the assembly to load and execute @@ -51,45 +50,53 @@ debug_address="127.0.0.1:10000" # If 1 and debug_enabled is 1, Mono debugger server will suspend the game execution until a debugger is attached debug_suspend="0" +# CoreCLR options (IL2CPP) + +# Path to coreclr shared library WITHOUT THE EXTENSION that contains the CoreCLR runtime +coreclr_path="" + +# Path to the directory containing the managed core libraries for CoreCLR (mscorlib, System, etc.) +corlib_dir="" + ################################################################################ # Everything past this point is the actual script - -# Special case: program is launched via Steam -# In that case rerun the script via their bootstrapper to ensure Steam overlay works -if [ "$2" = "SteamLaunch" ]; then - # Conceptually: exec "$1" "$2" "$3" "$4" "$0" "rest of $@" - # But newer versions of Steam interleave the $1..$4 with some "--" arguments, so preserve them as well - # Bash has array subscripting, but POSIX sh doesn't, so avoid it - to_rotate=4 - rotated=0 - while [ $((to_rotate-=1)) -ge 0 ]; do - while [ "z$1" = "z--" ]; do - set -- "$@" "$1" - shift - rotated=$((rotated+1)) +set -e + +# Special case: program is launched via Steam on Linux +# In that case rerun the script via their bootstrapper to delay adding Doorstop to LD_PRELOAD +# This is required until https://github.com/NeighTools/UnityDoorstop/issues/88 is resolved +for a in "$@"; do + if [ "$a" = "SteamLaunch" ]; then + rotated=0; max=$# + while [ $rotated -lt $max ]; do + # Test if argument is prefixed with the value of $PWD + if [ "$1" != "${1#"${PWD%/}/"}" ]; then + to_rotate=$(($# - rotated)) + set -- "$@" "$0" + while [ $((to_rotate-=1)) -ge 0 ]; do + set -- "$@" "$1" + shift + done + exec "$@" + else + set -- "$@" "$1" + shift + rotated=$((rotated+1)) + fi done - set -- "$@" "$1" - shift - rotated=$((rotated+1)) - done - to_rotate=$(($# - rotated)) - set -- "$@" "$0" - while [ $((to_rotate-=1)) -ge 0 ]; do - set -- "$@" "$1" - shift - done - exec "$@" -fi + echo "Could not determine game executable launched by Steam" 1>&2 + exit 1 + fi +done # Handle first param being executable name if [ -x "$1" ] ; then executable_name="$1" - echo "Target executable: $1" shift fi if [ -z "${executable_name}" ] || [ ! -x "${executable_name}" ]; then - echo "Please set executable_name to a valid name in a text editor or as the first command line parameter" + echo "Please set executable_name to a valid name in a text editor or as the first command line parameter" 1>&2 exit 1 fi @@ -100,50 +107,56 @@ arch="" executable_path="" lib_extension="" -# Set executable path and the extension to use for the libdoorstop shared object +abs_path() { + # Resolve relative path to absolute from BASEDIR + if [ "$1" = "${1#/}" ]; then + set -- "${BASEDIR}/${1}" + fi + echo "$(cd "$(dirname "$1")" && pwd)/$(basename "$1")" +} + +# Set executable path and the extension to use for the libdoorstop shared object as well as check whether we're running on Apple Silicon os_type="$(uname -s)" case ${os_type} in Linux*) - executable_path="${executable_name}" - # Handle relative paths - if ! echo "$executable_path" | grep "^/.*$"; then - executable_path="${BASEDIR}/${executable_path}" - fi + executable_path="$(abs_path "$executable_name")" lib_extension="so" ;; Darwin*) - real_executable_name="${executable_name}" - - # Handle relative directories - if ! echo "$real_executable_name" | grep "^/.*$"; then - real_executable_name="${BASEDIR}/${real_executable_name}" - fi + real_executable_name="$(abs_path "$executable_name")" # If we're not even an actual executable, check .app Info for actual executable - if ! echo "$real_executable_name" | grep "^.*\.app/Contents/MacOS/.*"; then - # Add .app to the end if not given - if ! echo "$real_executable_name" | grep "^.*\.app$"; then - real_executable_name="${real_executable_name}.app" - fi - inner_executable_name=$(defaults read "${real_executable_name}/Contents/Info" CFBundleExecutable) - executable_path="${real_executable_name}/Contents/MacOS/${inner_executable_name}" - else - executable_path="${executable_name}" - fi + case $real_executable_name in + *.app/Contents/MacOS/*) + executable_path="${executable_name}" + ;; + *) + # Add .app to the end if not given + if [ "$real_executable_name" = "${real_executable_name%.app}" ]; then + real_executable_name="${real_executable_name}.app" + fi + inner_executable_name=$(defaults read "${real_executable_name}/Contents/Info" CFBundleExecutable) + executable_path="${real_executable_name}/Contents/MacOS/${inner_executable_name}" + ;; + esac lib_extension="dylib" + + # CPUs for Apple Silicon are in the format "Apple M.." + cpu_type="$(sysctl -n machdep.cpu.brand_string)" + case "${cpu_type}" in + Apple*) + is_apple_silicon=1 + ;; + esac ;; *) # alright whos running games on freebsd - echo "Unknown operating system ($(uname -s))" - echo "Make an issue at https://github.com/NeighTools/UnityDoorstop" + echo "Unknown operating system ($(uname -s))" 1>&2 + echo "Make an issue at https://github.com/NeighTools/UnityDoorstop" 1>&2 exit 1 ;; esac -abs_path() { - echo "$(cd "$(dirname "$1")" && pwd)/$(basename "$1")" -} - _readlink() { # relative links with readlink (without -f) do not preserve the path info ab_path="$(abs_path "$1")" @@ -155,7 +168,6 @@ _readlink() { echo "$link" } - resolve_executable_path () { e_path="$(abs_path "$1")" @@ -165,13 +177,23 @@ resolve_executable_path () { echo "${e_path}" } -# Get absolute path of executable and show to user +# Get absolute path of executable executable_path=$(resolve_executable_path "${executable_path}") -echo "${executable_path}" # Figure out the arch of the executable with file file_out="$(LD_PRELOAD="" file -b "${executable_path}")" case "${file_out}" in + *PE32*) + echo "The executable is a Windows executable file. You must use Wine/Proton and BepInEx for Windows with this executable." 1>&2 + echo "Uninstall BepInEx for *nix and install BepInEx for Windows instead." 1>&2 + echo "More info: https://docs.bepinex.dev/articles/advanced/steam_interop.html#protonwine" 1>&2 + exit 1 + ;; + *shell\ script*) + # Fallback for games that launch a shell script from Steam + # default to x64, change as needed + arch="x64" + ;; *64-bit*) arch="x64" ;; @@ -179,10 +201,10 @@ case "${file_out}" in arch="x86" ;; *) - echo "The executable \"${executable_path}\" is not compiled for x86 or x64 (might be ARM?)" - echo "If you think this is a mistake (or would like to encourage support for other architectures)" - echo "Please make an issue at https://github.com/NeighTools/UnityDoorstop" - echo "Got: ${file_out}" + echo "The executable \"${executable_path}\" is not compiled for x86 or x64 (might be ARM?)" 1>&2 + echo "If you think this is a mistake (or would like to encourage support for other architectures)" 1>&2 + echo "Please make an issue at https://github.com/NeighTools/UnityDoorstop" 1>&2 + echo "Got: ${file_out}" 1>&2 exit 1 ;; esac @@ -200,49 +222,78 @@ doorstop_bool() { } # Read from command line -while :; do +i=0; max=$# +while [ $i -lt $max ]; do case "$1" in - --doorstop_enabled) + --doorstop_enabled) # For backwards compatibility. Renamed to --doorstop-enabled enabled="$(doorstop_bool "$2")" shift + i=$((i+1)) ;; - --doorstop_target_assembly) + --doorstop_target_assembly) # For backwards compatibility. Renamed to --doorstop-target-assembly target_assembly="$2" shift + i=$((i+1)) + ;; + --doorstop-enabled) + enabled="$(doorstop_bool "$2")" + shift + i=$((i+1)) + ;; + --doorstop-target-assembly) + target_assembly="$2" + shift + i=$((i+1)) ;; --doorstop-boot-config-override) boot_config_override="$2" shift + i=$((i+1)) ;; --doorstop-mono-dll-search-path-override) dll_search_path_override="$2" shift + i=$((i+1)) ;; --doorstop-mono-debug-enabled) debug_enable="$(doorstop_bool "$2")" shift + i=$((i+1)) ;; --doorstop-mono-debug-suspend) debug_suspend="$(doorstop_bool "$2")" shift + i=$((i+1)) ;; --doorstop-mono-debug-address) debug_address="$2" shift + i=$((i+1)) + ;; + --doorstop-clr-runtime-coreclr-path) + coreclr_path="$2" + shift + i=$((i+1)) + ;; + --doorstop-clr-corlib-dir) + corlib_dir="$2" + shift + i=$((i+1)) ;; *) - if [ -z "$1" ]; then - break - fi - rest_args="$rest_args $1" + set -- "$@" "$1" ;; esac shift + i=$((i+1)) done +target_assembly="$(abs_path "$target_assembly")" + # Move variables to environment export DOORSTOP_ENABLED="$enabled" export DOORSTOP_TARGET_ASSEMBLY="$target_assembly" +export DOORSTOP_BOOT_CONFIG_OVERRIDE="$boot_config_override" export DOORSTOP_IGNORE_DISABLED_ENV="$ignore_disable_switch" export DOORSTOP_MONO_DLL_SEARCH_PATH_OVERRIDE="$dll_search_path_override" export DOORSTOP_MONO_DEBUG_ENABLED="$debug_enable" @@ -269,5 +320,14 @@ else export DYLD_INSERT_LIBRARIES="${doorstop_name}:${DYLD_INSERT_LIBRARIES}" fi -# shellcheck disable=SC2086 -exec "$executable_path" $rest_args \ No newline at end of file +if [ -n "${is_apple_silicon}" ]; then + export ARCHPREFERENCE="arm64e,arm64,x86_64" + + # We need to use arch for Apple Silicon to allow the executable to be run natively as otherwise if + # the executable is universal, supporting both x86_64 and arm64, MacOs will still run it as x86_64 + # if the parent process is running as x86. + # arch also strips the DYLD_INSERT_LIBRARIES env var so we have to pass that in manually + exec arch -e DYLD_INSERT_LIBRARIES="${DYLD_INSERT_LIBRARIES}" "$executable_path" "$@" +else + exec "$executable_path" "$@" +fi diff --git a/scripts/valheim/build_doorstop_arm64e.sh b/scripts/valheim/build_doorstop_arm64e.sh new file mode 100755 index 0000000..39d97a9 --- /dev/null +++ b/scripts/valheim/build_doorstop_arm64e.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd -P)" +UNITY_DOORSTOP_TAG="${UNITY_DOORSTOP_TAG:-v4.5.0}" +WORKDIR="${1:-${TMPDIR:-/tmp}/UnityDoorstop-arm64e}" + +rm -rf "$WORKDIR" +git clone --depth 1 --branch "$UNITY_DOORSTOP_TAG" https://github.com/NeighTools/UnityDoorstop "$WORKDIR" + +# Newer Apple Silicon machines can require arm64e for injected dylibs. +perl -0pi -e 's/set_arch\("arm64"\)/set_arch("arm64e")/' "$WORKDIR/xmake.lua" + +( + cd "$WORKDIR" + ./build.sh +) + +OUTPUT_DIR="$WORKDIR/build/macosx/universal/release" +if ! lipo -archs "$OUTPUT_DIR/libdoorstop.dylib" | grep -q 'arm64e'; then + echo "Expected arm64e slice in $OUTPUT_DIR/libdoorstop.dylib" >&2 + exit 1 +fi + +echo "$OUTPUT_DIR" diff --git a/scripts/valheim/install_gale_export.sh b/scripts/valheim/install_gale_export.sh new file mode 100755 index 0000000..5bd9bea --- /dev/null +++ b/scripts/valheim/install_gale_export.sh @@ -0,0 +1,203 @@ +#!/usr/bin/env bash +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd -P)" +DEFAULT_APP="/Users/$USER/Library/Application Support/Steam/steamapps/common/Valheim/valheim.app" +APP_PATH="$DEFAULT_APP" +DOORSTOP_DIR="" +EXPORT_PATH="" +SKIP_RUNTIME_INSTALL=0 +WITH_SMOKE_TESTS=0 +WORKDIR="${TMPDIR:-/tmp}/valheim-gale-export" + +usage() { + cat <<'EOF' +Usage: + install_gale_export.sh --export /path/to/profile.r2z [--app /path/to/valheim.app] [--doorstop-dir DIR] + [--skip-runtime-install] [--with-smoke-tests] + +Examples: + scripts/valheim/install_gale_export.sh --export ~/Downloads/Servidor.r2z + scripts/valheim/install_gale_export.sh --export ~/Downloads/Servidor.r2z --app "/Users/me/Library/Application Support/Steam/steamapps/common/Valheim/valheim.app" +EOF +} + +while [ $# -gt 0 ]; do + case "$1" in + --export) + EXPORT_PATH="$2" + shift 2 + ;; + --app) + APP_PATH="$2" + shift 2 + ;; + --doorstop-dir) + DOORSTOP_DIR="$2" + shift 2 + ;; + --skip-runtime-install) + SKIP_RUNTIME_INSTALL=1 + shift + ;; + --with-smoke-tests) + WITH_SMOKE_TESTS=1 + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "Unknown argument: $1" >&2 + usage >&2 + exit 1 + ;; + esac +done + +if [ -z "$EXPORT_PATH" ]; then + echo "Missing required --export argument" >&2 + usage >&2 + exit 1 +fi + +if [ ! -f "$EXPORT_PATH" ]; then + echo "Export file not found: $EXPORT_PATH" >&2 + exit 1 +fi + +if [ -d "$APP_PATH/Contents/MacOS" ]; then + MACOS_DIR="$APP_PATH/Contents/MacOS" +else + echo "Could not resolve a Valheim app bundle from: $APP_PATH" >&2 + exit 1 +fi + +if [ "$SKIP_RUNTIME_INSTALL" = "0" ]; then + install_args=("$APP_PATH") + if [ -n "$DOORSTOP_DIR" ]; then + install_args+=(--doorstop-dir "$DOORSTOP_DIR") + fi + if [ "$WITH_SMOKE_TESTS" = "1" ]; then + install_args+=(--with-smoke-tests) + fi + + "$REPO_ROOT/scripts/valheim/install_macos_arm64.sh" "${install_args[@]}" +fi + +rm -rf "$WORKDIR" +mkdir -p "$WORKDIR/export" "$WORKDIR/downloads" "$WORKDIR/packages" "$WORKDIR/stage/BepInEx/plugins" "$WORKDIR/stage/BepInEx/patchers" "$WORKDIR/stage/BepInEx/config" "$WORKDIR/stage/BepInEx/core" +ditto -x -k "$EXPORT_PATH" "$WORKDIR/export" + +MANIFEST_PATH="$WORKDIR/export/export.r2x" +if [ ! -f "$MANIFEST_PATH" ]; then + echo "Could not find export.r2x inside $EXPORT_PATH" >&2 + exit 1 +fi + +if [ -d "$WORKDIR/export/BepInEx/plugins" ]; then + rsync -a "$WORKDIR/export/BepInEx/plugins/" "$WORKDIR/stage/BepInEx/plugins/" +fi +if [ -d "$WORKDIR/export/BepInEx/patchers" ]; then + rsync -a "$WORKDIR/export/BepInEx/patchers/" "$WORKDIR/stage/BepInEx/patchers/" +fi +if [ -d "$WORKDIR/export/BepInEx/config" ]; then + rsync -a "$WORKDIR/export/BepInEx/config/" "$WORKDIR/stage/BepInEx/config/" +fi +if [ -d "$WORKDIR/export/BepInEx/core" ]; then + rsync -a "$WORKDIR/export/BepInEx/core/" "$WORKDIR/stage/BepInEx/core/" +fi + +package_rows=() +while IFS= read -r line; do + package_rows+=("$line") +done < <( + ruby - "$MANIFEST_PATH" <<'RUBY' +require "yaml" + +manifest = YAML.safe_load(File.read(ARGV[0])) +mods = manifest.fetch("mods", []) + +mods.each do |mod| + next unless mod["enabled"] + + author, package = mod.fetch("name").split("-", 2) + version = mod.fetch("version") + + puts [author, package, "#{version["major"]}.#{version["minor"]}.#{version["patch"]}"].join("\t") +end +RUBY +) + +if [ "${#package_rows[@]}" -eq 0 ]; then + echo "No enabled packages found in $MANIFEST_PATH" >&2 + exit 1 +fi + +downloaded_count=0 +for row in "${package_rows[@]}"; do + IFS=$'\t' read -r author package version <<<"$row" + + if [ "$author" = "denikson" ] && [ "$package" = "BepInExPack_Valheim" ]; then + echo "Skipping runtime package $author-$package-$version; using custom macOS runtime instead" + continue + fi + + archive_path="$WORKDIR/downloads/${author}-${package}-${version}.zip" + package_dir="$WORKDIR/packages/${author}-${package}-${version}" + package_url="https://thunderstore.io/package/download/${author}/${package}/${version}/" + + echo "Downloading $author-$package-$version" + curl -fsSL "$package_url" -o "$archive_path" + rm -rf "$package_dir" + mkdir -p "$package_dir" + ditto -x -k "$archive_path" "$package_dir" + + while IFS= read -r windows_path; do + relative_path="${windows_path#$package_dir/}" + normalized_relative_path="${relative_path//\\//}" + normalized_path="$package_dir/$normalized_relative_path" + mkdir -p "$(dirname "$normalized_path")" + mv "$windows_path" "$normalized_path" + done < <(find "$package_dir" -type f -name '*\\*') + + find "$package_dir" -maxdepth 1 -type f \( -name '*.dll' -o -name '*.pdb' -o -name '*.mdb' -o -name '*.xml' \) -exec cp {} "$WORKDIR/stage/BepInEx/plugins/" \; + + for relative_dir in plugins patchers config core; do + if [ -d "$package_dir/$relative_dir" ]; then + rsync -a "$package_dir/$relative_dir/" "$WORKDIR/stage/BepInEx/$relative_dir/" + fi + if [ -d "$package_dir/BepInEx/$relative_dir" ]; then + rsync -a "$package_dir/BepInEx/$relative_dir/" "$WORKDIR/stage/BepInEx/$relative_dir/" + fi + done + + downloaded_count=$((downloaded_count + 1)) +done + +PROFILE_BACKUP_DIR="$MACOS_DIR/.bepinex-profile-backup-$(date +%Y%m%d-%H%M%S)" +mkdir -p "$PROFILE_BACKUP_DIR" +for relative_dir in plugins patchers config; do + if [ -d "$MACOS_DIR/BepInEx/$relative_dir" ]; then + rsync -a "$MACOS_DIR/BepInEx/$relative_dir/" "$PROFILE_BACKUP_DIR/$relative_dir/" + fi + rm -rf "$MACOS_DIR/BepInEx/$relative_dir" + mkdir -p "$MACOS_DIR/BepInEx/$relative_dir" +done + +if [ -d "$WORKDIR/stage/BepInEx/core" ] && [ -n "$(find "$WORKDIR/stage/BepInEx/core" -mindepth 1 -maxdepth 1 -print -quit)" ]; then + rsync -a "$WORKDIR/stage/BepInEx/core/" "$MACOS_DIR/BepInEx/core/" +fi +rsync -a "$WORKDIR/stage/BepInEx/plugins/" "$MACOS_DIR/BepInEx/plugins/" +rsync -a "$WORKDIR/stage/BepInEx/patchers/" "$MACOS_DIR/BepInEx/patchers/" +rsync -a "$WORKDIR/stage/BepInEx/config/" "$MACOS_DIR/BepInEx/config/" + +echo +echo "Installed Gale export from: $EXPORT_PATH" +echo "Downloaded packages: $downloaded_count" +echo "Profile backup: $PROFILE_BACKUP_DIR" +echo "Valheim path: $MACOS_DIR" +echo +echo "Run Valheim with:" +echo " cd \"$MACOS_DIR\" && ./run_bepinex.sh ./Valheim" diff --git a/scripts/valheim/install_macos_arm64.sh b/scripts/valheim/install_macos_arm64.sh new file mode 100755 index 0000000..943d10c --- /dev/null +++ b/scripts/valheim/install_macos_arm64.sh @@ -0,0 +1,111 @@ +#!/usr/bin/env bash +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd -P)" +DEFAULT_APP="/Users/$USER/Library/Application Support/Steam/steamapps/common/Valheim/valheim.app" +APP_PATH="$DEFAULT_APP" +DOORSTOP_DIR="" +MODS_DIR="" +WITH_SMOKE_TESTS=0 +STAGE_DIR="${TMPDIR:-/tmp}/bepinex-valheim-macos-arm64" + +usage() { + cat <<'EOF' +Usage: + install_macos_arm64.sh [path/to/valheim.app] [--doorstop-dir DIR] [--mods-dir DIR] [--with-smoke-tests] + +Examples: + scripts/valheim/install_macos_arm64.sh + scripts/valheim/install_macos_arm64.sh "/Users/me/Library/Application Support/Steam/steamapps/common/Valheim/valheim.app" --with-smoke-tests + scripts/valheim/install_macos_arm64.sh --mods-dir /path/to/exported/BepInEx/plugins +EOF +} + +while [ $# -gt 0 ]; do + case "$1" in + --doorstop-dir) + DOORSTOP_DIR="$2" + shift 2 + ;; + --mods-dir) + MODS_DIR="$2" + shift 2 + ;; + --with-smoke-tests) + WITH_SMOKE_TESTS=1 + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + APP_PATH="$1" + shift + ;; + esac +done + +if [ -d "$APP_PATH/Contents/MacOS" ]; then + MACOS_DIR="$APP_PATH/Contents/MacOS" +elif [ -d "$APP_PATH" ] && [ -x "$APP_PATH/Valheim" ]; then + MACOS_DIR="$APP_PATH" +else + echo "Could not resolve a Valheim app bundle from: $APP_PATH" >&2 + exit 1 +fi + +mkdir -p "$STAGE_DIR/BepInEx/core" "$STAGE_DIR/BepInEx/plugins" "$STAGE_DIR/BepInEx/patchers" +rm -rf "$STAGE_DIR/BepInEx/core"/* + +dotnet publish "$REPO_ROOT/BepInEx.Preloader/BepInEx.Preloader.csproj" -c Release -o "$STAGE_DIR/BepInEx/core" +cp "$REPO_ROOT/doorstop/run_bepinex.sh" "$STAGE_DIR/run_bepinex.sh" +chmod +x "$STAGE_DIR/run_bepinex.sh" + +if [ -z "$DOORSTOP_DIR" ]; then + if [ "$(sysctl -n machdep.ptrauth_enabled 2>/dev/null || echo 0)" = "1" ]; then + DOORSTOP_DIR="$("$REPO_ROOT/scripts/valheim/build_doorstop_arm64e.sh" "${TMPDIR:-/tmp}/UnityDoorstop-arm64e")" + else + OFFICIAL_DIR="${TMPDIR:-/tmp}/doorstop-macos-official" + rm -rf "$OFFICIAL_DIR" + mkdir -p "$OFFICIAL_DIR" + curl -L "https://github.com/NeighTools/UnityDoorstop/releases/download/v4.5.0/doorstop_macos_release_4.5.0.zip" -o "$OFFICIAL_DIR/doorstop_macos.zip" + unzip -q -o "$OFFICIAL_DIR/doorstop_macos.zip" -d "$OFFICIAL_DIR/unpacked" + DOORSTOP_DIR="$OFFICIAL_DIR/unpacked/universal" + fi +fi + +cp "$DOORSTOP_DIR/libdoorstop.dylib" "$STAGE_DIR/libdoorstop.dylib" +cp "$DOORSTOP_DIR/.doorstop_version" "$STAGE_DIR/.doorstop_version" + +if [ "$WITH_SMOKE_TESTS" = "1" ]; then + dotnet build "$REPO_ROOT/TestPlugins/ValheimArm64Smoke.NoHarmony/ValheimArm64Smoke.NoHarmony.csproj" -c Release + dotnet build "$REPO_ROOT/TestPlugins/ValheimArm64Smoke.Harmony/ValheimArm64Smoke.Harmony.csproj" -c Release + cp "$REPO_ROOT/TestPlugins/ValheimArm64Smoke.NoHarmony/bin/Release/net35/ValheimArm64Smoke.NoHarmony.dll" "$STAGE_DIR/BepInEx/plugins/" + cp "$REPO_ROOT/TestPlugins/ValheimArm64Smoke.Harmony/bin/Release/net35/ValheimArm64Smoke.Harmony.dll" "$STAGE_DIR/BepInEx/plugins/" +fi + +if [ -n "$MODS_DIR" ]; then + find "$MODS_DIR" -maxdepth 1 -type f \( -name '*.dll' -o -name '*.pdb' -o -name '*.mdb' \) -exec cp {} "$STAGE_DIR/BepInEx/plugins/" \; +fi + +BACKUP_DIR="" +for path in BepInEx libdoorstop.dylib run_bepinex.sh .doorstop_version; do + if [ -e "$MACOS_DIR/$path" ]; then + if [ -z "$BACKUP_DIR" ]; then + BACKUP_DIR="$MACOS_DIR/.bepinex-backup-$(date +%Y%m%d-%H%M%S)" + mkdir -p "$BACKUP_DIR" + fi + rsync -a "$MACOS_DIR/$path" "$BACKUP_DIR/" + fi +done + +rsync -a "$STAGE_DIR/" "$MACOS_DIR/" + +echo "Installed BepInEx into: $MACOS_DIR" +if [ -n "$BACKUP_DIR" ]; then + echo "Backup created at: $BACKUP_DIR" +fi +echo +echo "Run Valheim with:" +echo " cd \"$MACOS_DIR\" && ./run_bepinex.sh ./Valheim" diff --git a/submodules/BepInEx.Harmony b/submodules/BepInEx.Harmony index 6830e68..f16083f 160000 --- a/submodules/BepInEx.Harmony +++ b/submodules/BepInEx.Harmony @@ -1 +1 @@ -Subproject commit 6830e686c7931164a4e34595a34cbfa15846d1d4 +Subproject commit f16083f4e78f50320afad47a5d0e1c5474a582f5