Attempt to merge BepInEx master

This commit is contained in:
ManlyMarco committed 2026-06-11 16:27:35 +02:00
commit d0f1eabd78
36 files changed
+1638 -347

No files matched your search

+5 -5
View File
@@ -34,9 +34,9 @@ jobs:
branch: master
workflow_id: build.yml
github_token: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/setup-dotnet@v1
- uses: actions/setup-dotnet@v4
with:
dotnet-version: "6.0.x"
dotnet-version: "8.0.x"
- name: Build
run: |
./build.sh --target Publish --build-type ${{ steps.info.outputs.build_type }} --build-id ${{ steps.info.outputs.build_id }} --last-build-commit \"${{ steps.last_successful_commit.outputs.commit_hash }}\" --nuget-api-key \"${{ secrets.BEPIN_DEV_NUGET_KEY }}\"
@@ -51,10 +51,10 @@ jobs:
id: generate_changelog
if: ${{ steps.info.outputs.build_type == 'BleedingEdge' }}
run: |
changelog=$(git --no-pager log --no-merges --pretty="format:- (\`%h\`) [%an] %s" ${{ steps.last_successful_commit.outputs.commit_hash }}..HEAD | sed ':a;N;$!ba;s/"/\"/g' | sed ':a;N;$!ba;s/\n/\\n/g')
changelog=$(git --no-pager log --no-merges --pretty="format:- (\`%h\`) [%an] %s" ${{ steps.last_successful_commit.outputs.commit_hash }}..HEAD | sed ':a;N;$!ba;s/"/\\"/g' | sed ':a;N;$!ba;s/\n/\\n/g')
echo "::set-output name=changelog::$changelog"
- name: Notify success
uses: joelwmale/webhook-action@2.1.0
uses: joelwmale/webhook-action@2.4.1
env:
BEPINEX_DISCORD_WEBHOOK: ${{ secrets.BEPINEX_DISCORD_WEBHOOK }}
if: ${{ steps.info.outputs.build_type == 'BleedingEdge' && env.BEPINEX_DISCORD_WEBHOOK != null }}
@@ -72,7 +72,7 @@ jobs:
]
}
- name: Upload Artifacts
uses: actions/upload-artifact@v2.2.4
uses: actions/upload-artifact@v4
with:
path: "./bin/dist/*.zip"
name: "BepInEx_CI_${{ steps.info.outputs.build_type }}_${{ steps.info.outputs.sha_short }}_${{ steps.info.outputs.build_id || 0 }}"
+3 -3
View File
@@ -10,14 +10,14 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2.3.5
- uses: actions/setup-dotnet@v1
- uses: actions/setup-dotnet@v4
with:
dotnet-version: "6.0.x"
dotnet-version: "8.0.x"
- name: Build
run: |
./build.sh --target Publish
- name: Upload Artifacts
uses: actions/upload-artifact@v2.2.4
uses: actions/upload-artifact@v4
with:
path: "./bin/dist/*.zip"
name: "BepInEx_CI"
+2 -2
View File
@@ -8,9 +8,9 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" PrivateAssets="All"/>
<PackageReference Include="HarmonyX" Version="2.10.1"/>
<PackageReference Include="HarmonyX" Version="2.10.2" />
<PackageReference Include="SemanticVersioning" Version="2.0.2"/>
<PackageReference Include="MonoMod.Utils" Version="22.5.1.1"/>
<PackageReference Include="MonoMod.Utils" Version="22.7.31.1"/>
</ItemGroup>
<ItemGroup>
<Compile Remove="Contract\IPlugin.cs"/>
+38 -10
View File
@@ -315,7 +315,15 @@ public abstract class BaseChainloader<TPlugin>
var plugins = DiscoverPlugins();
Logger.Log(LogLevel.Info, $"{plugins.Count} plugin{(plugins.Count == 1 ? "" : "s")} to load");
LoadPlugins(plugins);
Finished?.Invoke();
try
{
Finished?.Invoke();
}
catch (Exception ex)
{
Logger.Log(LogLevel.Error, $"Error occurred in a Chainloader.Finished event handler: {ex}");
}
}
catch (Exception ex)
{
@@ -344,6 +352,7 @@ public abstract class BaseChainloader<TPlugin>
{
var dependsOnInvalidPlugin = false;
var missingDependencies = new List<BepInDependency>();
var incompatibleDependencies = new List<KeyValuePair<BepInDependency, string>>();
foreach (var dependency in plugin.Dependencies)
{
static bool IsHardDependency(BepInDependency dep) =>
@@ -359,15 +368,22 @@ public abstract class BaseChainloader<TPlugin>
pluginVersion = pluginInfo?.Metadata.Version;
}
if (!dependencyExists || dependency.VersionRange != null &&
!dependency.VersionRange.IsSatisfied(pluginVersion))
// The dependency is not installed at all.
if (!dependencyExists)
{
// If the dependency is hard, collect it into a list to show
if (IsHardDependency(dependency))
missingDependencies.Add(dependency);
continue;
}
// The dependency is installed, but its version does not satisfy the requested range.
if (dependency.VersionRange != null && !dependency.VersionRange.IsSatisfied(pluginVersion))
{
if (IsHardDependency(dependency))
incompatibleDependencies.Add(new KeyValuePair<BepInDependency, string>(dependency, pluginVersion?.ToString()));
continue;
}
// If the dependency is a hard and is invalid (e.g. has missing dependencies), report that to the user
if (invalidPlugins.Contains(dependency.DependencyGUID) && IsHardDependency(dependency))
{
@@ -387,13 +403,25 @@ public abstract class BaseChainloader<TPlugin>
continue;
}
if (missingDependencies.Count != 0)
if (missingDependencies.Count != 0 || incompatibleDependencies.Count != 0)
{
var message = $@"Could not load [{plugin}] because it has missing dependencies: {
string.Join(", ", missingDependencies.Select(s => s.VersionRange == null ? s.DependencyGUID : $"{s.DependencyGUID} ({s.VersionRange})").ToArray())
}";
DependencyErrors.Add(message);
Logger.Log(LogLevel.Error, message);
if (missingDependencies.Count != 0)
{
var message = $@"Could not load [{plugin}] because it has missing dependencies: {
string.Join(", ", missingDependencies.Select(s => s.VersionRange == null ? s.DependencyGUID : $"{s.DependencyGUID} ({s.VersionRange})").ToArray())
}. Install the listed plugin(s) and restart the game.";
DependencyErrors.Add(message);
Logger.Log(LogLevel.Error, message);
}
if (incompatibleDependencies.Count != 0)
{
var message = $@"Could not load [{plugin}] because the following dependencies are installed with an incompatible version: {
string.Join(", ", incompatibleDependencies.Select(s => $"{s.Key.DependencyGUID} (found {s.Value}, requires {s.Key.VersionRange})").ToArray())
}. Update the listed plugin(s) to a version that satisfies the requirement.";
DependencyErrors.Add(message);
Logger.Log(LogLevel.Error, message);
}
invalidPlugins.Add(plugin.Metadata.GUID);
continue;
+20 -2
View File
@@ -34,7 +34,7 @@ public class ConfigFile : IDictionary<ConfigDefinition, ConfigEntryBase>
if (File.Exists(ConfigFilePath))
Reload();
else if (saveOnInit) Save();
else if (saveOnInit) TrySave();
}
public static ConfigFile CoreConfig { get; } = new(Paths.BepInExConfigPath, true);
@@ -352,6 +352,24 @@ public class ConfigFile : IDictionary<ConfigDefinition, ConfigEntryBase>
}
}
/// <summary>
/// Writes the config to disk like <see cref="Save" />, but logs and swallows I/O failures (e.g. a read-only
/// or locked config file) instead of throwing. Used only for the creation-time saves (initial file write and
/// new-entry binds), which can run before logging is up; explicit saves and setting changes still throw.
/// </summary>
private void TrySave()
{
try
{
Save();
}
catch (Exception e) when (e is IOException or UnauthorizedAccessException)
{
Logger.Log(LogLevel.Warning,
$"Could not write config file {ConfigFilePath}: {e.Message}. Continuing with the in-memory configuration.");
}
}
#endregion
#region Wraps
@@ -452,7 +470,7 @@ public class ConfigFile : IDictionary<ConfigDefinition, ConfigEntryBase>
}
if (SaveOnConfigSet)
Save();
TrySave();
return entry;
}
@@ -41,7 +41,7 @@ internal partial class ConsoleEncoding : Encoding
public override int GetByteCount(char[] chars, int index, int count)
{
WriteCharBuffer(chars, index, count);
var result = WideCharToMultiByte(_codePage, 0, chars, count, _zeroByte, 0, IntPtr.Zero, IntPtr.Zero);
var result = WideCharToMultiByte(_codePage, 0, _charBuffer, count, _zeroByte, 0, IntPtr.Zero, IntPtr.Zero);
return result;
}
@@ -50,7 +50,7 @@ internal partial class ConsoleEncoding : Encoding
var byteCount = GetByteCount(chars, charIndex, charCount);
WriteCharBuffer(chars, charIndex, charCount);
ExpandByteBuffer(byteCount);
_ = WideCharToMultiByte(_codePage, 0, chars, charCount, _byteBuffer, byteCount, IntPtr.Zero,
_ = WideCharToMultiByte(_codePage, 0, _charBuffer, charCount, _byteBuffer, byteCount, IntPtr.Zero,
IntPtr.Zero);
var readCount = Math.Min(bytes.Length, byteCount);
ReadByteBuffer(bytes, byteIndex, readCount);
@@ -60,7 +60,7 @@ internal partial class ConsoleEncoding : Encoding
public override int GetCharCount(byte[] bytes, int index, int count)
{
WriteByteBuffer(bytes, index, count);
var result = MultiByteToWideChar(_codePage, 0, bytes, count, _zeroChar, 0);
var result = MultiByteToWideChar(_codePage, 0, _byteBuffer, count, _zeroChar, 0);
return result;
}
@@ -69,7 +69,7 @@ internal partial class ConsoleEncoding : Encoding
var charCount = GetCharCount(bytes, byteIndex, byteCount);
WriteByteBuffer(bytes, byteIndex, byteCount);
ExpandCharBuffer(charCount);
_ = MultiByteToWideChar(_codePage, 0, bytes, byteCount, _charBuffer, charCount);
_ = MultiByteToWideChar(_codePage, 0, _byteBuffer, byteCount, _charBuffer, charCount);
var readCount = Math.Min(chars.Length, charCount);
ReadCharBuffer(chars, charIndex, readCount);
return readCount;
@@ -92,8 +92,9 @@ internal class WindowsConsoleDriver : IConsoleDriver
// Make sure of ConsoleEncoding helper class because on some Monos
// Encoding.GetEncoding throws NotImplementedException on most codepages
// NOTE: We don't set Console.OutputEncoding because it resets any existing Console.Out writers
if (!useManagedEncoder)
ConsoleEncoding.ConsoleCodePage = codepage;
// Always set the console output codepage so the Windows console interprets bytes correctly,
// regardless of whether we use a managed encoder or ConsoleEncoding to produce those bytes.
ConsoleEncoding.ConsoleCodePage = codepage;
// If stdout exists, write to it, otherwise make it the same as console out
// Not sure if this is needed? Does the original Console.Out still work?
+12 -2
View File
@@ -120,8 +120,18 @@ public class BepInDependency : Attribute, ICacheable
/// not load and an error will be logged instead.
/// </summary>
/// <param name="guid">The GUID of the referenced plugin.</param>
/// <param name="version">The version range of the referenced plugin.</param>
/// <remarks>When version is supplied the dependency is always treated as HardDependency</remarks>
/// <param name="version">
/// The version requirement of the referenced plugin, parsed as a SemVer range
/// (see <see href="https://github.com/adamreeve/semver.net#ranges" />). A plain version such as
/// <c>1.2.0</c> requires that exact version; use a range such as <c>&gt;=1.2.0</c>, <c>1.2.*</c>,
/// <c>~1.2.0</c> or <c>^1.2.0</c> to accept more than one version.
/// </param>
/// <remarks>
/// When a version is supplied the dependency is always treated as a hard dependency.
/// Plugins migrating from BepInEx 5 should note a behaviour change: a bare version was previously
/// treated as a minimum (<c>&gt;=</c>), whereas in BepInEx 6 it is an exact match. Use
/// <c>&gt;=1.2.0</c> to keep the old behaviour.
/// </remarks>
public BepInDependency(string guid, string version) : this(guid)
{
VersionRange = Range.Parse(version);
+151 -32
View File
@@ -1,4 +1,5 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using BepInEx.Core.Logging.Interpolation;
@@ -25,7 +26,7 @@ public static class Logger
/// <summary>
/// Log levels that are currently listened to by at least one listener.
/// </summary>
public static LogLevel ListenedLogLevels => listeners.activeLogLevels;
public static LogLevel ListenedLogLevels => listeners.ActiveLogLevels;
/// <summary>
/// Collection of all log listeners that receive log events.
@@ -39,10 +40,7 @@ public static class Logger
internal static void InternalLogEvent(object sender, LogEventArgs eventArgs)
{
// ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator Prevent extra allocations
foreach (var listener in listeners)
if ((eventArgs.Level & listener.LogLevelFilter) != LogLevel.None)
listener?.LogEvent(sender, eventArgs);
listeners.SendLogEvent(sender, eventArgs);
}
/// <summary>
@@ -74,69 +72,190 @@ public static class Logger
return source;
}
private class LogListenerCollection : List<ILogListener>, ICollection<ILogListener>
private class LogListenerCollection : ThreadSafeCollection<ILogListener>
{
public LogLevel activeLogLevels = LogLevel.None;
internal LogLevel ActiveLogLevels = LogLevel.None;
void ICollection<ILogListener>.Add(ILogListener item)
internal void SendLogEvent(object sender, LogEventArgs eventArgs)
{
// Do this instead of foreach to avoid boxing, also very slightly faster
var aListInTime = BaseList;
// ReSharper disable once ForCanBeConvertedToForeach
for (int i = 0; i < aListInTime.Count; i++)
{
if ((eventArgs.Level & aListInTime[i].LogLevelFilter) != LogLevel.None)
aListInTime[i].LogEvent(sender, eventArgs);
}
}
public override void Add(ILogListener item)
{
if (item == null)
throw new ArgumentNullException(nameof(item));
activeLogLevels |= item.LogLevelFilter;
base.Add(item);
lock (SpinLock)
{
ActiveLogLevels |= item.LogLevelFilter;
base.Add(item);
}
}
void ICollection<ILogListener>.Clear()
public override void Clear()
{
activeLogLevels = LogLevel.None;
base.Clear();
lock (SpinLock)
{
ActiveLogLevels = LogLevel.None;
base.Clear();
}
}
bool ICollection<ILogListener>.Remove(ILogListener item)
public override bool Remove(ILogListener item)
{
if (item == null || !base.Remove(item))
return false;
activeLogLevels = LogLevel.None;
lock (SpinLock)
{
ActiveLogLevels = LogLevel.None;
foreach (var i in this)
activeLogLevels |= i.LogLevelFilter;
foreach (var i in this)
ActiveLogLevels |= i.LogLevelFilter;
return true;
return true;
}
}
}
private class LogSourceCollection : List<ILogSource>, ICollection<ILogSource>
private class LogSourceCollection : ThreadSafeCollection<ILogSource>
{
void ICollection<ILogSource>.Add(ILogSource item)
public override void Add(ILogSource item)
{
if (item == null)
throw new ArgumentNullException(nameof(item),
"Log sources cannot be null when added to the source list.");
item.LogEvent += InternalLogEvent;
base.Add(item);
lock (SpinLock)
{
item.LogEvent += InternalLogEvent;
base.Add(item);
var copy = new List<ILogSource>(BaseList.Count + 1);
copy.AddRange(BaseList);
copy.Add(item);
BaseList = copy;
}
}
void ICollection<ILogSource>.Clear()
public override void Clear()
{
foreach (var item in this)
item.LogEvent -= InternalLogEvent;
if (Count == 0)
return;
base.Clear();
lock (SpinLock)
{
for (var i = 0; i < BaseList.Count; i++)
BaseList[i].LogEvent -= InternalLogEvent;
BaseList = [];
}
}
bool ICollection<ILogSource>.Remove(ILogSource item)
public override bool Remove(ILogSource item)
{
if (item == null || !Remove(item))
if (item == null)
return false;
item.LogEvent -= InternalLogEvent;
return true;
lock (SpinLock)
{
var wasPresent = base.Remove(item);
if (wasPresent)
item.LogEvent -= InternalLogEvent;
return wasPresent;
}
}
}
/// <summary>
/// Simple thread safe list that prioritizes read speed over write speed.
/// Read is the same as a normal list, while write locks and allocates a copy of the list.
/// Logger lists are rarely updated so this tradeoff should be fine.
/// </summary>
/// <inheritdoc />
private class ThreadSafeCollection<T> : ICollection<T> where T : class
{
protected readonly object SpinLock = new();
protected List<T> BaseList = [];
public int Count => BaseList.Count;
public bool IsReadOnly => false;
public IEnumerator<T> GetEnumerator()
{
// Can't avoid boxing
return BaseList.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
// Can't avoid boxing
return BaseList.GetEnumerator();
}
public virtual void Add(T item)
{
if (item == null)
throw new ArgumentNullException(nameof(item), "item can't be null");
lock (SpinLock)
{
var copy = new List<T>(BaseList.Count + 1);
copy.AddRange(BaseList);
copy.Add(item);
BaseList = copy;
}
}
public virtual void Clear()
{
if (Count == 0)
return;
lock (SpinLock)
BaseList = [];
}
public bool Contains(T item)
{
return BaseList.Contains(item);
}
public void CopyTo(T[] array, int arrayIndex)
{
BaseList.CopyTo(array, arrayIndex);
}
public virtual bool Remove(T item)
{
if (item == null)
return false;
lock (SpinLock)
{
var copy = new List<T>(BaseList.Count);
var any = false;
for (int i = 0; i < BaseList.Count; i++)
{
var existingItem = BaseList[i];
if (existingItem.Equals(item))
any = true;
else
copy.Add(existingItem);
}
BaseList = copy;
return any;
}
}
}
}
+24 -4
View File
@@ -1,4 +1,4 @@
using System.IO;
using System.IO;
using System.Linq;
using System.Reflection;
using MonoMod.Utils;
@@ -19,6 +19,12 @@ public static class Paths
Version.Parse(MetadataHelper.GetAttributes<AssemblyInformationalVersionAttribute>(typeof(Paths).Assembly)[0]
.InformationalVersion);
/// <summary>
/// BepInEx version without the build suffix.
/// </summary>
public static Version DisplayBepInExVersion { get; } =
new(BepInExVersion.Major, BepInExVersion.Minor, BepInExVersion.Patch, BepInExVersion.PreRelease);
/// <summary>
/// The path to the Managed folder that contains the main managed assemblies.
/// </summary>
@@ -106,9 +112,23 @@ public static class Paths
? Utility.ParentDirectory(executablePath, 4)
: Path.GetDirectoryName(executablePath);
GameDataPath = managedPath != null && gameDataRelativeToManaged
? Path.GetDirectoryName(managedPath)
: Path.Combine(GameRootPath, $"{ProcessName}_Data");
if (managedPath != null && gameDataRelativeToManaged)
{
GameDataPath = Path.GetDirectoryName(managedPath);
}
else
{
// According to some experiments, Unity checks whether globalgamemanagers/data.unity3d exists in the data folder before picking it.
// 'ProcessName_Data' folder is checked first, then if that fails 'Data' folder is checked. If neither is valid, the player crashes.
// A simple Directory.Exists check is accurate enough while being less likely to break in case these conditions change.
GameDataPath = Path.Combine(GameRootPath, $"{ProcessName}_Data");
if (!Directory.Exists(GameDataPath))
GameDataPath = Path.Combine(GameRootPath, "Data");
}
if (string.IsNullOrEmpty(GameDataPath) || !Directory.Exists(GameDataPath))
throw new DirectoryNotFoundException("Failed to extract valid GameDataPath from executablePath: " + executablePath);
ManagedPath = managedPath ?? Path.Combine(GameDataPath, "Managed");
BepInExRootPath = bepinRootPath ?? Path.Combine(GameRootPath, "BepInEx");
ConfigPath = Path.Combine(BepInExRootPath, "config");
+15 -4
View File
@@ -6,6 +6,7 @@ using System.Reflection;
using System.Reflection.Emit;
using System.Security.Cryptography;
using System.Text;
using BepInEx.Logging;
using Mono.Cecil;
namespace BepInEx;
@@ -416,11 +417,21 @@ public static class Utility
{
var result = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase);
foreach (var directory in directories)
foreach (var file in Directory.GetFiles(directory, pattern))
{
var fileName = Path.GetFileName(file);
if (!result.ContainsKey(fileName))
result[fileName] = file;
// A configured search directory (e.g. a missing unstripped_corlib) may not exist; warn and skip it
// instead of letting Directory.GetFiles throw DirectoryNotFoundException.
if (!Directory.Exists(directory))
{
Logger.Log(LogLevel.Warning, $"Skipping search directory that does not exist: {directory}");
continue;
}
foreach (var file in Directory.GetFiles(directory, pattern))
{
var fileName = Path.GetFileName(file);
if (!result.ContainsKey(fileName))
result[fileName] = file;
}
}
return result.Values;
@@ -8,8 +8,8 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" PrivateAssets="All"/>
<PackageReference Include="HarmonyX" Version="2.10.1"/>
<PackageReference Include="MonoMod.RuntimeDetour" Version="22.5.1.1"/>
<PackageReference Include="MonoMod.Utils" Version="22.5.1.1"/>
<PackageReference Include="HarmonyX" Version="2.10.2" />
<PackageReference Include="MonoMod.RuntimeDetour" Version="22.7.31.1"/>
<PackageReference Include="MonoMod.Utils" Version="22.7.31.1"/>
</ItemGroup>
</Project>
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using BepInEx.Logging;
@@ -34,17 +35,14 @@ public static class ChainloaderLogHelper
public static void PrintLogInfo(ManualLogSource log)
{
var bepinVersion = Paths.BepInExVersion;
var versionMini = new SemanticVersioning.Version(bepinVersion.Major, bepinVersion.Minor, bepinVersion.Patch,
bepinVersion.PreRelease);
var consoleTitle = $"BepInEx {versionMini} - {Paths.ProcessName}";
log.Log(LogLevel.Message, consoleTitle);
var consoleTitle = $"BepInEx {Paths.DisplayBepInExVersion} - {Paths.ProcessName}";
log.Log(LogLevel.Message, $"{consoleTitle} ({File.GetLastWriteTime(Paths.ExecutablePath)})");
if (ConsoleManager.ConsoleActive)
ConsoleManager.SetConsoleTitle(consoleTitle);
if (!string.IsNullOrEmpty(bepinVersion.Build))
log.Log(LogLevel.Message, $"Built from commit {bepinVersion.Build}");
if (!string.IsNullOrEmpty(Paths.BepInExVersion.Build))
log.Log(LogLevel.Message, $"Built from commit {Paths.BepInExVersion.Build}");
Logger.Log(LogLevel.Info, $"System platform: {GetPlatformString()}");
Logger.Log(LogLevel.Info,
@@ -82,7 +82,7 @@ public class PatcherPluginInfoAttribute : Attribute
/// <summary>
/// Defines an assembly that a patch method will target.
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
public class TargetAssemblyAttribute : Attribute
{
/// <summary>
@@ -108,7 +108,7 @@ public class TargetAssemblyAttribute : Attribute
/// <summary>
/// Defines a type that a patch method will target.
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
public class TargetTypeAttribute : Attribute
{
/// <param name="targetAssembly">The short filename of the assembly of which <paramref name="targetType" /> belongs to.</param>
+6 -1
View File
@@ -80,7 +80,12 @@ internal static class PlatformUtils
if (wineGetVersion != IntPtr.Zero)
{
current |= Platform.Wine;
var getVersion = wineGetVersion.AsDelegate<GetWineVersionDelegate>();
// It's not safe to use the AsDelegate() extension method here because:
// - It comes from the MonoMod.Utils.DynDll class, defined in MonoMod.Common.
// - The DynDll class has a static constructor that reads PlatformHelper.Current.
// - Reading from that property freezes it: subsequent writes will throw an exception.
// - This method only sets PlatformHelper.Current at the very end.
var getVersion = Marshal.GetDelegateForFunctionPointer(wineGetVersion, typeof(GetWineVersionDelegate)) as GetWineVersionDelegate;
WineVersion = getVersion();
}
}
+3 -3
View File
@@ -57,9 +57,9 @@ A more comprehensive comparison list of features and compatibility is available
## Used libraries
- [NeighTools/UnityDoorstop](https://github.com/NeighTools/UnityDoorstop) - v4.0.0
- [BepInEx/HarmonyX](https://github.com/BepInEx/HarmonyX) - v2.10.1
- [0x0ade/MonoMod](https://github.com/0x0ade/MonoMod) - v22.5.1.1
- [NeighTools/UnityDoorstop](https://github.com/NeighTools/UnityDoorstop) - v4.5.0
- [BepInEx/HarmonyX](https://github.com/BepInEx/HarmonyX) - v2.10.2
- [0x0ade/MonoMod](https://github.com/0x0ade/MonoMod) - v22.7.31.1
- [jbevain/cecil](https://github.com/jbevain/cecil) - v0.10.4
#### IL2CPP libraries
@@ -6,7 +6,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="HarmonyX" Version="2.10.1"/>
<PackageReference Include="HarmonyX" Version="2.10.2" />
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" PrivateAssets="All"/>
</ItemGroup>
@@ -14,7 +14,7 @@
<ProjectReference Include="..\..\..\BepInEx.Preloader.Core\BepInEx.Preloader.Core.csproj" PrivateAssets="All"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="HarmonyX" Version="2.10.1"/>
<PackageReference Include="HarmonyX" Version="2.10.2" />
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" PrivateAssets="All"/>
</ItemGroup>
<Import Project="..\BepInEx.NET.Shared\BepInEx.NET.Shared.projitems" Label="Shared"/>
@@ -6,9 +6,9 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AssetRipper.VersionUtilities" Version="1.2.1"/>
<PackageReference Include="AssetRipper.Primitives" Version="3.1.3" />
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" PrivateAssets="all"/>
<PackageReference Include="MonoMod.Utils" Version="22.5.1.1"/>
<PackageReference Include="MonoMod.Utils" Version="22.7.31.1"/>
</ItemGroup>
</Project>
@@ -1,9 +1,9 @@
using System;
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.CompilerServices;
using System.Text;
using AssetRipper.VersionUtilities;
using AssetRipper.Primitives;
using MonoMod.Utils;
[assembly: InternalsVisibleTo("BepInEx.Unity.Mono.Preloader")]
@@ -13,16 +13,16 @@
<ProjectReference Include="..\BepInEx.Unity.Common\BepInEx.Unity.Common.csproj"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="HarmonyX" Version="2.10.1"/>
<PackageReference Include="Iced" Version="1.18.0"/>
<PackageReference Include="Il2CppInterop.Generator" Version="1.4.6"/>
<PackageReference Include="Il2CppInterop.HarmonySupport" Version="1.4.6"/>
<PackageReference Include="HarmonyX" Version="2.10.2" />
<PackageReference Include="Iced" Version="1.21.0" />
<PackageReference Include="Il2CppInterop.Generator" Version="1.5.1-ci.829"/>
<PackageReference Include="Il2CppInterop.HarmonySupport" Version="1.5.1-ci.829"/>
<PackageReference Include="Il2CppInterop.ReferenceLibs" Version="1.0.0" IncludeAssets="compile" PrivateAssets="all"/>
<PackageReference Include="Il2CppInterop.Runtime" Version="1.4.6"/>
<PackageReference Include="Il2CppInterop.Runtime" Version="1.5.1-ci.829"/>
<PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0" IncludeAssets="compile" PrivateAssets="all"/>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.1" IncludeAssets="compile" PrivateAssets="all"/>
<PackageReference Include="MonoMod.RuntimeDetour" Version="22.5.1.1"/>
<PackageReference Include="Samboy063.Cpp2IL.Core" Version="2022.1.0-development.866"/>
<PackageReference Include="MonoMod.RuntimeDetour" Version="22.7.31.1"/>
<PackageReference Include="Samboy063.Cpp2IL.Core" Version="2022.1.0-development.1452"/>
<PackageReference Include="Il2CppDumper" Version="1.0.0" />
</ItemGroup>
@@ -83,21 +83,19 @@ public class IL2CPPChainloader : BaseChainloader<BasePlugin>
if (methodName == "Internal_ActiveSceneChanged")
try
{
if (ConfigUnityLogging.Value)
{
Logger.Sources.Add(new IL2CPPUnityLogSource());
Application.CallLogCallback("Test call after applying unity logging hook", "", LogType.Assert,
true);
}
// Unhook up front so the detour fires once even if setup below throws.
unhook = true;
// Isolated so a missing-interop JIT failure happens inside the try (caught), not in OnInvokeMethod itself.
SetupUnityLogging();
Il2CppInteropManager.PreloadInteropAssemblies();
Instance.Execute();
}
catch (Exception ex)
{
Logger.Log(LogLevel.Fatal, "Unable to execute IL2CPP chainloader");
Logger.Log(LogLevel.Fatal, "Unable to execute IL2CPP chainloader, no plugins will be loaded");
Logger.Log(LogLevel.Error, ex);
}
@@ -113,6 +111,17 @@ public class IL2CPPChainloader : BaseChainloader<BasePlugin>
return result;
}
// JIT-compiled only when called here, so OnInvokeMethod needs no interop assemblies present to JIT.
private static void SetupUnityLogging()
{
if (!ConfigUnityLogging.Value)
return;
Logger.Sources.Add(new IL2CPPUnityLogSource());
Application.CallLogCallback("Test call after applying unity logging hook", "", LogType.Assert, true);
}
protected override void InitializeLoggers()
{
base.InitializeLoggers();
@@ -3,11 +3,14 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net.Http;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using BepInEx.Configuration;
using BepInEx.Logging;
using BepInEx.Unity.Common;
@@ -55,9 +58,10 @@ internal static partial class Il2CppInteropManager
"IL2CPP", "UnityBaseLibrariesSource",
"https://unity.bepinex.dev/libraries/{VERSION}.zip",
new StringBuilder()
.AppendLine("URL to the ZIP of managed Unity base libraries.")
.AppendLine("The base libraries are used by Il2CppInterop to generate interop assemblies.")
.AppendLine("URL to a ZIP file with managed Unity base libraries. They are used by Il2CppInterop to generate interop assemblies.")
.AppendLine("The URL can include {VERSION} template which will be replaced with the game's Unity engine version.")
.AppendLine("If a .zip file with the same filename as the URL (after template replacement) already exists in unity-libs, it will be used instead of downloading a new copy.")
.AppendLine("If you want to ensure BepInEx doesn't try to connect to the internet, set this to only the .zip filename (without a URL) and manually place the file in the unity-libs directory.")
.ToString());
private static readonly ConfigEntry<string> ConfigUnhollowerDeobfuscationRegex = ConfigFile.CoreConfig.Bind(
@@ -89,6 +93,25 @@ internal static partial class Il2CppInteropManager
.AppendLine("{ProcessName} - Name of the current process")
.AppendLine("{ApplicationData} - Path to the user AppData/Roaming folder")
.ToString());
private static readonly ConfigEntry<bool> PreloadIL2CPPInteropAssemblies = ConfigFile.CoreConfig.Bind(
"IL2CPP", "PreloadIL2CPPInteropAssemblies",
true,
new StringBuilder()
.AppendLine("Automatically load all interop assemblies right before loading plugins.")
.AppendLine("Some plugins may not work properly without this, but it may cause issues in some games.")
.ToString());
private static readonly ConfigEntry<string> GlobalMetadataPath = ConfigFile.CoreConfig.Bind(
"IL2CPP", "GlobalMetadataPath",
"{GameDataPath}/il2cpp_data/Metadata/global-metadata.dat",
new StringBuilder()
.AppendLine("The path to the IL2CPP metadata file.")
.AppendLine("Supports the following placeholders:")
.AppendLine("{BepInEx} - Path to the BepInEx folder.")
.AppendLine("{ProcessName} - Name of the current process")
.AppendLine("{GameDataPath} - Path to the game's Data folder.")
.ToString());
private static readonly ConfigEntry<bool> UseRuntimeAssemblies = ConfigFile.CoreConfig.Bind(
"IL2CPP", "UseRuntimeAssemblies",
@@ -140,10 +163,8 @@ internal static partial class Il2CppInteropManager
private static string HashPath => Path.Combine(IL2CPPInteropAssemblyPath, "assembly-hash.txt");
private static string IL2CPPBasePath
{
get
{
private static string IL2CPPBasePath {
get {
if (il2cppInteropBasePath != null)
return il2cppInteropBasePath;
var path = Utility.GetCommandLineArgValue("--unhollowed-path") ?? IL2CPPInteropAssembliesPath.Value;
@@ -160,6 +181,8 @@ internal static partial class Il2CppInteropManager
internal static string IL2CPPRuntimeBinariesPath => Path.Combine(IL2CPPBasePath, "runtime-bins");
private static string RenameMapPath => Path.Combine(Paths.BepInExRootPath, "DeobfuscationMap.csv.gz");
private static ILoggerFactory LoggerFactory { get; } = MSLoggerFactory.Create(b =>
{
b.AddProvider(new BepInExLoggerProvider())
@@ -196,6 +219,11 @@ internal static partial class Il2CppInteropManager
HashFile(md5, file);
}
if (File.Exists(RenameMapPath))
{
HashFile(md5, RenameMapPath);
}
// Hash some common dependencies as they can affect output
HashString(md5, typeof(InteropAssemblyGenerator).Assembly.GetName().Version.ToString());
HashString(md5, typeof(Cpp2IlApi).Assembly.GetName().Version.ToString());
@@ -256,10 +284,10 @@ internal static partial class Il2CppInteropManager
var unityVersion = UnityInfo.Version;
Il2CppInteropRuntime.Create(new RuntimeConfiguration
{
UnityVersion = new Version(unityVersion.Major, unityVersion.Minor, unityVersion.Build),
DetourProvider = new Il2CppInteropDetourProvider()
})
{
UnityVersion = new Version(unityVersion.Major, unityVersion.Minor, unityVersion.Build),
DetourProvider = new Il2CppInteropDetourProvider()
})
.AddLogger(interopLogger)
.AddHarmonySupport()
.Start();
@@ -277,17 +305,17 @@ internal static partial class Il2CppInteropManager
AppDomain.CurrentDomain.AddCecilPlatformAssemblies(UnityBaseLibsDirectory);
DownloadUnityAssemblies();
var cecilAssemblies = UseRuntimeAssemblies.Value ? RunIl2CppDumperWithRuntimeBinaries() : new AsmToCecilConverter(RunCpp2Il()).ConvertAll();
var asmResolverAssemblies = UseRuntimeAssemblies.Value ? RunIl2CppDumperWithRuntimeBinaries() : RunCpp2Il();
if (DumpDummyAssemblies.Value)
{
var dummyPath = Path.Combine(IL2CPPBasePath, "dummy");
Directory.CreateDirectory(dummyPath);
foreach (var assemblyDefinition in cecilAssemblies)
assemblyDefinition.Write(Path.Combine(dummyPath, $"{assemblyDefinition.Name.Name}.dll"));
foreach (var assemblyDefinition in asmResolverAssemblies)
assemblyDefinition.Write(Path.Combine(dummyPath, $"{assemblyDefinition.Name}.dll"));
}
RunIl2CppInteropGenerator(cecilAssemblies);
RunIl2CppInteropGenerator(asmResolverAssemblies);
File.WriteAllText(HashPath, ComputeHash());
}
@@ -297,38 +325,72 @@ internal static partial class Il2CppInteropManager
}
}
private static void DownloadUnityAssemblies()
{
private static void DownloadUnityAssemblies() {
var unityVersion = UnityInfo.Version;
var source =
UnityBaseLibrariesSource.Value.Replace("{VERSION}",
$"{unityVersion.Major}.{unityVersion.Minor}.{unityVersion.Build}");
var version = $"{unityVersion.Major}.{unityVersion.Minor}.{unityVersion.Build}";
var source = UnityBaseLibrariesSource.Value.Replace("{VERSION}", version);
if (string.IsNullOrEmpty(source)) return;
if (!string.IsNullOrEmpty(source))
var baseFolder = Directory.CreateDirectory(UnityBaseLibsDirectory);
baseFolder.EnumerateFiles("*.dll").Do(a=>a.Delete());
var uriIsValid = Uri.TryCreate(source, UriKind.Absolute, out var uri) && (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps);
var fileName = Path.GetFileName(uriIsValid ? uri.AbsolutePath : source);
var zipFilePath = Path.Combine(baseFolder.FullName, fileName);
if (!File.Exists(zipFilePath))
{
Logger.LogMessage("Downloading unity base libraries");
Directory.CreateDirectory(UnityBaseLibsDirectory);
Directory.EnumerateFiles(UnityBaseLibsDirectory, "*.dll").Do(File.Delete);
// Check if URI is valid before attempting download
if (!uriIsValid)
throw new ArgumentException($"Unity base libraries source \"{source}\" is not a valid URL and the .zip file does not exist locally. Either provide a valid HTTP/HTTPS URL, or place the .zip file in the unity-libs directory.");
Logger.LogMessage($"Downloading unity base libraries from {source}");
using var httpClient = new HttpClient();
using var zipStream = httpClient.GetStreamAsync(source).GetAwaiter().GetResult();
using var zipArchive = new ZipArchive(zipStream, ZipArchiveMode.Read);
Logger.LogMessage("Extracting downloaded unity base libraries");
zipArchive.ExtractToDirectory(UnityBaseLibsDirectory);
Stream zipStream;
try
{
zipStream = httpClient.GetStreamAsync(uri).GetAwaiter().GetResult();
}
catch (Exception e)
{
throw new IOException(
$"Failed to download Unity base libraries for Unity {unityVersion} from {source}: {e.Message}. " +
$"Base libraries for this Unity version may not be available on the server yet. Download a matching " +
$"base-libraries .zip and place it in the \"{UnityBaseLibsDirectory}\" (unity-libs) directory, or set " +
$"the [IL2CPP] UnityBaseLibrariesSource config option to a valid URL.", e);
}
using (zipStream)
{
try
{
using var writeStream = new FileStream(zipFilePath, FileMode.Create, FileAccess.Write, FileShare.None);
zipStream.CopyTo(writeStream);
}
catch
{
// Delete the incomplete file to avoid issues on next startup
try { File.Delete(zipFilePath); } catch { }
throw;
}
}
}
Logger.LogMessage($"Extracting unity base libraries from {zipFilePath}");
using var readStream = File.OpenRead(zipFilePath);
using var zipArchive = new ZipArchive(readStream, ZipArchiveMode.Read);
zipArchive.ExtractToDirectory(UnityBaseLibsDirectory);
}
private static List<AsmResolver.DotNet.AssemblyDefinition> RunCpp2Il()
{
Logger.LogMessage("Running Cpp2IL to generate dummy assemblies");
var metadataPath = Path.Combine(Paths.GameRootPath,
$"{Paths.ProcessName}_Data",
"il2cpp_data",
"Metadata",
"global-metadata.dat");
GlobalMetadataPath.Value
.Replace("{BepInEx}", Paths.BepInExRootPath)
.Replace("{ProcessName}", Paths.ProcessName)
.Replace("{GameDataPath}", Paths.GameDataPath));
Logger.LogMessage("Running Cpp2IL to generate dummy assemblies from " + metadataPath);
var stopwatch = new Stopwatch();
stopwatch.Start();
@@ -359,7 +421,7 @@ internal static partial class Il2CppInteropManager
cpp2IlProcessingLayer.Process(Cpp2IlApi.CurrentAppContext);
}
var assemblies = new AsmResolverDummyDllOutputFormat().BuildAssemblies(Cpp2IlApi.CurrentAppContext);
var assemblies = new AsmResolverDllOutputFormatDefault().BuildAssemblies(Cpp2IlApi.CurrentAppContext);
LibCpp2IlMain.Reset();
Cpp2IlApi.CurrentAppContext = null;
@@ -370,7 +432,7 @@ internal static partial class Il2CppInteropManager
return assemblies;
}
private static List<AssemblyDefinition> RunIl2CppDumperWithRuntimeBinaries()
private static List<AsmResolver.DotNet.AssemblyDefinition> RunIl2CppDumperWithRuntimeBinaries()
{
var metadataPath = Path.Combine(Paths.GameRootPath,
$"{Paths.ProcessName}_Data",
@@ -423,7 +485,7 @@ internal static partial class Il2CppInteropManager
return assemblies;
}
private static void RunIl2CppInteropGenerator(List<AssemblyDefinition> sourceAssemblies)
private static void RunIl2CppInteropGenerator(List<AsmResolver.DotNet.AssemblyDefinition> sourceAssemblies)
{
var opts = new GeneratorOptions
{
@@ -436,11 +498,10 @@ internal static partial class Il2CppInteropManager
: null,
};
var renameMapLocation = Path.Combine(Paths.BepInExRootPath, "DeobfuscationMap.csv.gz");
if (File.Exists(renameMapLocation))
if (File.Exists(RenameMapPath))
{
Logger.LogInfo("Parsing deobfuscation rename mappings");
opts.ReadRenameMap(renameMapLocation);
opts.ReadRenameMap(RenameMapPath);
}
Logger.LogInfo("Generating interop assemblies");
@@ -451,7 +512,35 @@ internal static partial class Il2CppInteropManager
.AddLogger(logger)
.AddInteropAssemblyGenerator()
.Run();
}
sourceAssemblies.Do(x => x.Dispose());
internal static void PreloadInteropAssemblies()
{
if (!PreloadIL2CPPInteropAssemblies.Value)
return;
var sw = Stopwatch.StartNew();
var files = Directory.EnumerateFiles(IL2CPPInteropAssemblyPath);
var loaded = 0;
Parallel.ForEach(files, file =>
{
if (!file.EndsWith(".dll", StringComparison.OrdinalIgnoreCase)) return;
var name = Path.GetFileNameWithoutExtension(file);
if (name.Equals("netstandard", StringComparison.OrdinalIgnoreCase)) return;
if (name.Equals("Il2Cppnetstandard", StringComparison.OrdinalIgnoreCase)) return;
try
{
// Do not use LoadFrom since it overrides preloader patches
Assembly.Load(name);
Interlocked.Increment(ref loaded);
}
catch (Exception e)
{
Logger.LogWarning($"Failed to preload {file} - {e}");
}
});
Logger.LogDebug($"Preloaded {loaded} interop assemblies in {sw.ElapsedMilliseconds}ms");
}
}
@@ -16,6 +16,6 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" PrivateAssets="All"/>
<PackageReference Include="HarmonyX" Version="2.10.1"/>
<PackageReference Include="HarmonyX" Version="2.10.2" />
</ItemGroup>
</Project>
@@ -31,9 +31,9 @@ internal class EntrypointPatcher : BasePatcher
"The name of the method in the specified entrypoint assembly and type to hook and load Chainloader from.");
private static string DefaultEntrypointAssembly =>
UnityInfo.Version.IsLess(2017) ? "UnityEngine.dll" : "UnityEngine.CoreModule.dll";
UnityInfo.Version.LessThan(2017) ? "UnityEngine.dll" : "UnityEngine.CoreModule.dll";
private static string DefaultEntrypointType => UnityInfo.Version.IsLess(5) ? "MonoBehaviour" : "Application";
private static string DefaultEntrypointType => UnityInfo.Version.LessThan(5) ? "MonoBehaviour" : "Application";
private bool HasLoaded { get; set; }
@@ -22,7 +22,10 @@ internal static class UnityPreloaderRunner
"Mono.Cecil.dll",
"Mono.Cecil.Mdb.dll",
"Mono.Cecil.Pdb.dll",
"Mono.Cecil.Rocks.dll"
"Mono.Cecil.Rocks.dll",
"MonoMod.Utils.dll",
"MonoMod.RuntimeDetour.dll",
"0Harmony.dll"
};
private static void LoadCriticalAssemblies()
@@ -16,7 +16,8 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" PrivateAssets="All"/>
<PackageReference Include="HarmonyX" Version="2.10.1"/>
<PackageReference Include="HarmonyX" Version="2.10.2" />
<PackageReference Include="UnityEngine" Version="5.6.1" IncludeAssets="compile"/>
<PackageReference Include="Unity.InputSystem" version="1.5.0" IncludeAssets="compile"/>
</ItemGroup>
</Project>
@@ -35,17 +35,8 @@ public struct KeyboardShortcut
/// </summary>
public static readonly KeyboardShortcut Empty = new();
/// <summary>
/// All KeyCode values that can be used in a keyboard shortcut.
/// </summary>
public static readonly IEnumerable<KeyCode> AllKeyCodes = (KeyCode[]) Enum.GetValues(typeof(KeyCode));
// Don't block hotkeys if mouse is being pressed, e.g. when shooting and trying to strafe
private static readonly KeyCode[] _modifierBlockKeyCodes = AllKeyCodes.Except(new[]
{
KeyCode.Mouse0, KeyCode.Mouse1, KeyCode.Mouse2, KeyCode.Mouse3, KeyCode.Mouse4, KeyCode.Mouse5,
KeyCode.Mouse6, KeyCode.None
}).ToArray();
private static KeyCode[] modifierBlockKeyCodes;
private readonly KeyCode[] _allKeys;
@@ -122,7 +113,7 @@ public struct KeyboardShortcut
var mainKey = MainKey;
if (mainKey == KeyCode.None) return false;
return Input.GetKeyDown(mainKey) && ModifierKeyTest();
return UnityInput.Current.GetKeyDown(mainKey) && ModifierKeyTest();
}
/// <summary>
@@ -133,7 +124,7 @@ public struct KeyboardShortcut
var mainKey = MainKey;
if (mainKey == KeyCode.None) return false;
return Input.GetKey(mainKey) && ModifierKeyTest();
return UnityInput.Current.GetKey(mainKey) && ModifierKeyTest();
}
/// <summary>
@@ -144,7 +135,7 @@ public struct KeyboardShortcut
var mainKey = MainKey;
if (mainKey == KeyCode.None) return false;
return Input.GetKeyUp(mainKey) && ModifierKeyTest();
return UnityInput.Current.GetKeyUp(mainKey) && ModifierKeyTest();
}
private bool ModifierKeyTest()
@@ -152,10 +143,15 @@ public struct KeyboardShortcut
var allKeys = _allKeys;
var mainKey = MainKey;
var allModifiersPressed = allKeys.All(c => c == mainKey || Input.GetKey(c));
var allModifiersPressed = allKeys.All(c => c == mainKey || UnityInput.Current.GetKey(c));
if (!allModifiersPressed) return false;
var noOtherModifiersPressed = _modifierBlockKeyCodes.All(c => !Input.GetKey(c) || allKeys.Contains(c));
// Lazy init to make sure the game is initialized, and we're in main thread before calling UnityInput.Current
modifierBlockKeyCodes ??= UnityInput.Current.SupportedKeyCodes.Except([KeyCode.Mouse0, KeyCode.Mouse1,
KeyCode.Mouse2, KeyCode.Mouse3, KeyCode.Mouse4, KeyCode.Mouse5, KeyCode.Mouse6, KeyCode.None
]).ToArray();
bool noOtherModifiersPressed = modifierBlockKeyCodes.All(c => !UnityInput.Current.GetKey(c) || allKeys.Contains(c));
return noOtherModifiersPressed;
}
@@ -0,0 +1,854 @@
using System;
using System.Collections.Generic;
using System.Linq;
using BepInEx.Logging;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.Controls;
// ReSharper disable InconsistentNaming
namespace BepInEx
{
/// <summary>
/// Abstraction layer over Unity's input systems for use in universal plugins that need to use hotkeys.
/// It can use either Input or Unity.InputSystem, depending on what's available. Input is preferred.
/// WARNING: Use only inside of Unity's main thread!
/// </summary>
public class UnityInput
{
private static IInputSystem current;
/// <summary>
/// Best currently supported input system.
/// </summary>
public static IInputSystem Current
{
get
{
if (current == null)
{
try
{
Input.GetKeyDown(KeyCode.A);
current = new LegacyInputSystem();
Logging.Logger.Log(LogLevel.Debug, "[UnityInput] Using LegacyInputSystem");
}
catch (InvalidOperationException)
{
current = new NewInputSystem();
Logging.Logger.Log(LogLevel.Debug, "[UnityInput] Using NewInputSystem");
}
catch (Exception ex)
{
Logging.Logger.Log(LogLevel.Warning, "[UnityInput] Failed to detect available input systems - " + ex);
}
}
return current;
}
}
/// <summary>
/// True if the Input class is not disabled.
/// </summary>
public bool LegacyInputSystemAvailable => Current is LegacyInputSystem;
}
/// <summary>
/// Generic input system interface. Just barely good enough for hotkeys.
/// </summary>
public interface IInputSystem
{
// No easy way to use these with the new input system
//bool GetButton(string buttonName);
//bool GetButtonDown(string buttonName);
//bool GetButtonUp(string buttonName);
/// <inheritdoc cref="Input.GetKey(string)"/>
bool GetKey(string name);
/// <inheritdoc cref="Input.GetKey(KeyCode)"/>
bool GetKey(KeyCode key);
/// <inheritdoc cref="Input.GetKeyDown(string)"/>
bool GetKeyDown(string name);
/// <inheritdoc cref="Input.GetKeyDown(KeyCode)"/>
bool GetKeyDown(KeyCode key);
/// <inheritdoc cref="Input.GetKeyUp(string)"/>
bool GetKeyUp(string name);
/// <inheritdoc cref="Input.GetKeyUp(KeyCode)"/>
bool GetKeyUp(KeyCode key);
/// <inheritdoc cref="Input.GetMouseButton(int)"/>
bool GetMouseButton(int button);
/// <inheritdoc cref="Input.GetMouseButtonDown(int)"/>
bool GetMouseButtonDown(int button);
/// <inheritdoc cref="Input.GetMouseButtonUp(int)"/>
bool GetMouseButtonUp(int button);
/// <inheritdoc cref="Input.ResetInputAxes()"/>
void ResetInputAxes();
/// <inheritdoc cref="Input.mousePosition"/>
Vector3 mousePosition { get; }
/// <inheritdoc cref="Input.mouseScrollDelta"/>
Vector2 mouseScrollDelta { get; }
/// <inheritdoc cref="Input.mousePresent"/>
bool mousePresent { get; }
/// <inheritdoc cref="Input.anyKey"/>
bool anyKey { get; }
/// <inheritdoc cref="Input.anyKeyDown"/>
bool anyKeyDown { get; }
/// <summary>
/// All KeyCodes supported by the current input system.
/// </summary>
IEnumerable<KeyCode> SupportedKeyCodes { get; }
}
internal class NewInputSystem : IInputSystem
{
public bool GetKey(string name) => GetControl(name)?.isPressed ?? false;
public bool GetKey(KeyCode key) => GetControl(key)?.isPressed ?? false;
public bool GetKeyDown(string name) => GetControl(name)?.wasPressedThisFrame ?? false;
public bool GetKeyDown(KeyCode key) => GetControl(key)?.wasPressedThisFrame ?? false;
public bool GetKeyUp(string name) => GetControl(name)?.wasReleasedThisFrame ?? false;
public bool GetKeyUp(KeyCode key) => GetControl(key)?.wasReleasedThisFrame ?? false;
public bool GetMouseButton(int button) => GetControl(KeyCode.Mouse0 + button)?.isPressed ?? false;
public bool GetMouseButtonDown(int button) => GetControl(KeyCode.Mouse0 + button)?.wasPressedThisFrame ?? false;
public bool GetMouseButtonUp(int button) => GetControl(KeyCode.Mouse0 + button)?.wasReleasedThisFrame ?? false;
public void ResetInputAxes() { /*Not supported*/ }
public Vector3 mousePosition => Mouse.current.position.ReadValue();
public Vector2 mouseScrollDelta => Mouse.current.scroll.ReadValue();
public bool mousePresent => Mouse.current.enabled;
public bool anyKey
{
get
{
if (Keyboard.current.anyKey.isPressed)
return true;
var current = Mouse.current;
return current.leftButton.isPressed ||
current.rightButton.isPressed ||
current.forwardButton.isPressed ||
current.backButton.isPressed ||
current.middleButton.isPressed;
}
}
public bool anyKeyDown
{
get
{
if (Keyboard.current.anyKey.wasPressedThisFrame)
return true;
var current = Mouse.current;
return current.leftButton.wasPressedThisFrame ||
current.rightButton.wasPressedThisFrame ||
current.forwardButton.wasPressedThisFrame ||
current.backButton.wasPressedThisFrame ||
current.middleButton.wasPressedThisFrame;
}
}
public IEnumerable<KeyCode> SupportedKeyCodes { get; } = Enum.GetValues(typeof(KeyCode)).Cast<KeyCode>().Where(x => GetControl(x, true) != null).ToList();
private static ButtonControl GetControl(string name) => GetControl((KeyCode)Enum.Parse(typeof(KeyCode), name, true));
private static ButtonControl GetControl(KeyCode key, bool silent = false)
{
switch (key)
{
case KeyCode.None:
break;
case KeyCode.Backspace:
return Keyboard.current.backspaceKey;
case KeyCode.Delete:
return Keyboard.current.deleteKey;
case KeyCode.Tab:
return Keyboard.current.tabKey;
case KeyCode.Clear:
break;
case KeyCode.Return:
return Keyboard.current.enterKey;
case KeyCode.Pause:
return Keyboard.current.pauseKey;
case KeyCode.Escape:
return Keyboard.current.escapeKey;
case KeyCode.Space:
return Keyboard.current.spaceKey;
case KeyCode.Keypad0:
return Keyboard.current.numpad0Key;
case KeyCode.Keypad1:
return Keyboard.current.numpad1Key;
case KeyCode.Keypad2:
return Keyboard.current.numpad2Key;
case KeyCode.Keypad3:
return Keyboard.current.numpad3Key;
case KeyCode.Keypad4:
return Keyboard.current.numpad4Key;
case KeyCode.Keypad5:
return Keyboard.current.numpad5Key;
case KeyCode.Keypad6:
return Keyboard.current.numpad6Key;
case KeyCode.Keypad7:
return Keyboard.current.numpad7Key;
case KeyCode.Keypad8:
return Keyboard.current.numpad8Key;
case KeyCode.Keypad9:
return Keyboard.current.numpad9Key;
case KeyCode.KeypadPeriod:
return Keyboard.current.numpadPeriodKey;
case KeyCode.KeypadDivide:
return Keyboard.current.numpadDivideKey;
case KeyCode.KeypadMultiply:
return Keyboard.current.numpadMultiplyKey;
case KeyCode.KeypadMinus:
return Keyboard.current.numpadMinusKey;
case KeyCode.KeypadPlus:
return Keyboard.current.numpadPlusKey;
case KeyCode.KeypadEnter:
return Keyboard.current.numpadEnterKey;
case KeyCode.KeypadEquals:
return Keyboard.current.numpadEqualsKey;
case KeyCode.UpArrow:
return Keyboard.current.upArrowKey;
case KeyCode.DownArrow:
return Keyboard.current.downArrowKey;
case KeyCode.RightArrow:
return Keyboard.current.rightArrowKey;
case KeyCode.LeftArrow:
return Keyboard.current.leftArrowKey;
case KeyCode.Insert:
return Keyboard.current.insertKey;
case KeyCode.Home:
return Keyboard.current.homeKey;
case KeyCode.End:
return Keyboard.current.endKey;
case KeyCode.PageUp:
return Keyboard.current.pageUpKey;
case KeyCode.PageDown:
return Keyboard.current.pageDownKey;
case KeyCode.F1:
return Keyboard.current.f1Key;
case KeyCode.F2:
return Keyboard.current.f2Key;
case KeyCode.F3:
return Keyboard.current.f3Key;
case KeyCode.F4:
return Keyboard.current.f4Key;
case KeyCode.F5:
return Keyboard.current.f5Key;
case KeyCode.F6:
return Keyboard.current.f6Key;
case KeyCode.F7:
return Keyboard.current.f7Key;
case KeyCode.F8:
return Keyboard.current.f8Key;
case KeyCode.F9:
return Keyboard.current.f9Key;
case KeyCode.F10:
return Keyboard.current.f10Key;
case KeyCode.F11:
return Keyboard.current.f11Key;
case KeyCode.F12:
return Keyboard.current.f12Key;
case KeyCode.F13:
break;
case KeyCode.F14:
break;
case KeyCode.F15:
break;
case KeyCode.Alpha0:
return Keyboard.current.digit0Key;
case KeyCode.Alpha1:
return Keyboard.current.digit1Key;
case KeyCode.Alpha2:
return Keyboard.current.digit2Key;
case KeyCode.Alpha3:
return Keyboard.current.digit3Key;
case KeyCode.Alpha4:
return Keyboard.current.digit4Key;
case KeyCode.Alpha5:
return Keyboard.current.digit5Key;
case KeyCode.Alpha6:
return Keyboard.current.digit6Key;
case KeyCode.Alpha7:
return Keyboard.current.digit7Key;
case KeyCode.Alpha8:
return Keyboard.current.digit8Key;
case KeyCode.Alpha9:
return Keyboard.current.digit9Key;
case KeyCode.Exclaim:
break;
case KeyCode.DoubleQuote:
break;
case KeyCode.Hash:
break;
case KeyCode.Dollar:
break;
case KeyCode.Ampersand:
break;
case KeyCode.Quote:
return Keyboard.current.quoteKey;
case KeyCode.LeftParen:
break;
case KeyCode.RightParen:
break;
case KeyCode.Asterisk:
break;
case KeyCode.Plus:
return Keyboard.current.numpadPlusKey;
case KeyCode.Comma:
return Keyboard.current.commaKey;
case KeyCode.Minus:
return Keyboard.current.minusKey;
case KeyCode.Period:
return Keyboard.current.periodKey;
case KeyCode.Slash:
return Keyboard.current.slashKey;
case KeyCode.Colon:
break;
case KeyCode.Semicolon:
return Keyboard.current.semicolonKey;
case KeyCode.Less:
break;
case KeyCode.Equals:
return Keyboard.current.equalsKey;
case KeyCode.Greater:
break;
case KeyCode.Question:
break;
case KeyCode.At:
break;
case KeyCode.LeftBracket:
return Keyboard.current.leftBracketKey;
case KeyCode.Backslash:
return Keyboard.current.backslashKey;
case KeyCode.RightBracket:
return Keyboard.current.rightBracketKey;
case KeyCode.Caret:
break;
case KeyCode.Underscore:
break;
case KeyCode.BackQuote:
return Keyboard.current.backquoteKey;
case KeyCode.A:
return Keyboard.current.aKey;
case KeyCode.B:
return Keyboard.current.bKey;
case KeyCode.C:
return Keyboard.current.cKey;
case KeyCode.D:
return Keyboard.current.dKey;
case KeyCode.E:
return Keyboard.current.eKey;
case KeyCode.F:
return Keyboard.current.fKey;
case KeyCode.G:
return Keyboard.current.gKey;
case KeyCode.H:
return Keyboard.current.hKey;
case KeyCode.I:
return Keyboard.current.iKey;
case KeyCode.J:
return Keyboard.current.jKey;
case KeyCode.K:
return Keyboard.current.kKey;
case KeyCode.L:
return Keyboard.current.lKey;
case KeyCode.M:
return Keyboard.current.mKey;
case KeyCode.N:
return Keyboard.current.nKey;
case KeyCode.O:
return Keyboard.current.oKey;
case KeyCode.P:
return Keyboard.current.pKey;
case KeyCode.Q:
return Keyboard.current.qKey;
case KeyCode.R:
return Keyboard.current.rKey;
case KeyCode.S:
return Keyboard.current.sKey;
case KeyCode.T:
return Keyboard.current.tKey;
case KeyCode.U:
return Keyboard.current.uKey;
case KeyCode.V:
return Keyboard.current.vKey;
case KeyCode.W:
return Keyboard.current.wKey;
case KeyCode.X:
return Keyboard.current.xKey;
case KeyCode.Y:
return Keyboard.current.yKey;
case KeyCode.Z:
return Keyboard.current.zKey;
case KeyCode.Numlock:
return Keyboard.current.numLockKey;
case KeyCode.CapsLock:
return Keyboard.current.capsLockKey;
case KeyCode.ScrollLock:
return Keyboard.current.scrollLockKey;
case KeyCode.RightShift:
return Keyboard.current.rightShiftKey;
case KeyCode.LeftShift:
return Keyboard.current.leftShiftKey;
case KeyCode.RightControl:
return Keyboard.current.rightCtrlKey;
case KeyCode.LeftControl:
return Keyboard.current.leftCtrlKey;
case KeyCode.RightAlt:
return Keyboard.current.rightAltKey;
case KeyCode.LeftAlt:
return Keyboard.current.leftAltKey;
case KeyCode.LeftCommand:
return Keyboard.current.leftCommandKey;
case KeyCode.LeftWindows:
return Keyboard.current.leftWindowsKey;
case KeyCode.RightCommand:
return Keyboard.current.rightCommandKey;
case KeyCode.RightWindows:
return Keyboard.current.rightWindowsKey;
case KeyCode.AltGr:
break;
case KeyCode.Help:
break;
case KeyCode.Print:
return Keyboard.current.printScreenKey;
case KeyCode.SysReq:
break;
case KeyCode.Break:
break;
case KeyCode.Menu:
return Keyboard.current.contextMenuKey;
case KeyCode.Mouse0:
return Mouse.current.leftButton;
case KeyCode.Mouse1:
return Mouse.current.rightButton;
case KeyCode.Mouse2:
return Mouse.current.middleButton;
case KeyCode.Mouse3:
return Mouse.current.backButton;
case KeyCode.Mouse4:
return Mouse.current.forwardButton;
case KeyCode.Mouse5:
break;
case KeyCode.Mouse6:
break;
case KeyCode.JoystickButton0:
break;
case KeyCode.JoystickButton1:
break;
case KeyCode.JoystickButton2:
break;
case KeyCode.JoystickButton3:
break;
case KeyCode.JoystickButton4:
break;
case KeyCode.JoystickButton5:
break;
case KeyCode.JoystickButton6:
break;
case KeyCode.JoystickButton7:
break;
case KeyCode.JoystickButton8:
break;
case KeyCode.JoystickButton9:
break;
case KeyCode.JoystickButton10:
break;
case KeyCode.JoystickButton11:
break;
case KeyCode.JoystickButton12:
break;
case KeyCode.JoystickButton13:
break;
case KeyCode.JoystickButton14:
break;
case KeyCode.JoystickButton15:
break;
case KeyCode.JoystickButton16:
break;
case KeyCode.JoystickButton17:
break;
case KeyCode.JoystickButton18:
break;
case KeyCode.JoystickButton19:
break;
case KeyCode.Joystick1Button0:
break;
case KeyCode.Joystick1Button1:
break;
case KeyCode.Joystick1Button2:
break;
case KeyCode.Joystick1Button3:
break;
case KeyCode.Joystick1Button4:
break;
case KeyCode.Joystick1Button5:
break;
case KeyCode.Joystick1Button6:
break;
case KeyCode.Joystick1Button7:
break;
case KeyCode.Joystick1Button8:
break;
case KeyCode.Joystick1Button9:
break;
case KeyCode.Joystick1Button10:
break;
case KeyCode.Joystick1Button11:
break;
case KeyCode.Joystick1Button12:
break;
case KeyCode.Joystick1Button13:
break;
case KeyCode.Joystick1Button14:
break;
case KeyCode.Joystick1Button15:
break;
case KeyCode.Joystick1Button16:
break;
case KeyCode.Joystick1Button17:
break;
case KeyCode.Joystick1Button18:
break;
case KeyCode.Joystick1Button19:
break;
case KeyCode.Joystick2Button0:
break;
case KeyCode.Joystick2Button1:
break;
case KeyCode.Joystick2Button2:
break;
case KeyCode.Joystick2Button3:
break;
case KeyCode.Joystick2Button4:
break;
case KeyCode.Joystick2Button5:
break;
case KeyCode.Joystick2Button6:
break;
case KeyCode.Joystick2Button7:
break;
case KeyCode.Joystick2Button8:
break;
case KeyCode.Joystick2Button9:
break;
case KeyCode.Joystick2Button10:
break;
case KeyCode.Joystick2Button11:
break;
case KeyCode.Joystick2Button12:
break;
case KeyCode.Joystick2Button13:
break;
case KeyCode.Joystick2Button14:
break;
case KeyCode.Joystick2Button15:
break;
case KeyCode.Joystick2Button16:
break;
case KeyCode.Joystick2Button17:
break;
case KeyCode.Joystick2Button18:
break;
case KeyCode.Joystick2Button19:
break;
case KeyCode.Joystick3Button0:
break;
case KeyCode.Joystick3Button1:
break;
case KeyCode.Joystick3Button2:
break;
case KeyCode.Joystick3Button3:
break;
case KeyCode.Joystick3Button4:
break;
case KeyCode.Joystick3Button5:
break;
case KeyCode.Joystick3Button6:
break;
case KeyCode.Joystick3Button7:
break;
case KeyCode.Joystick3Button8:
break;
case KeyCode.Joystick3Button9:
break;
case KeyCode.Joystick3Button10:
break;
case KeyCode.Joystick3Button11:
break;
case KeyCode.Joystick3Button12:
break;
case KeyCode.Joystick3Button13:
break;
case KeyCode.Joystick3Button14:
break;
case KeyCode.Joystick3Button15:
break;
case KeyCode.Joystick3Button16:
break;
case KeyCode.Joystick3Button17:
break;
case KeyCode.Joystick3Button18:
break;
case KeyCode.Joystick3Button19:
break;
case KeyCode.Joystick4Button0:
break;
case KeyCode.Joystick4Button1:
break;
case KeyCode.Joystick4Button2:
break;
case KeyCode.Joystick4Button3:
break;
case KeyCode.Joystick4Button4:
break;
case KeyCode.Joystick4Button5:
break;
case KeyCode.Joystick4Button6:
break;
case KeyCode.Joystick4Button7:
break;
case KeyCode.Joystick4Button8:
break;
case KeyCode.Joystick4Button9:
break;
case KeyCode.Joystick4Button10:
break;
case KeyCode.Joystick4Button11:
break;
case KeyCode.Joystick4Button12:
break;
case KeyCode.Joystick4Button13:
break;
case KeyCode.Joystick4Button14:
break;
case KeyCode.Joystick4Button15:
break;
case KeyCode.Joystick4Button16:
break;
case KeyCode.Joystick4Button17:
break;
case KeyCode.Joystick4Button18:
break;
case KeyCode.Joystick4Button19:
break;
case KeyCode.Joystick5Button0:
break;
case KeyCode.Joystick5Button1:
break;
case KeyCode.Joystick5Button2:
break;
case KeyCode.Joystick5Button3:
break;
case KeyCode.Joystick5Button4:
break;
case KeyCode.Joystick5Button5:
break;
case KeyCode.Joystick5Button6:
break;
case KeyCode.Joystick5Button7:
break;
case KeyCode.Joystick5Button8:
break;
case KeyCode.Joystick5Button9:
break;
case KeyCode.Joystick5Button10:
break;
case KeyCode.Joystick5Button11:
break;
case KeyCode.Joystick5Button12:
break;
case KeyCode.Joystick5Button13:
break;
case KeyCode.Joystick5Button14:
break;
case KeyCode.Joystick5Button15:
break;
case KeyCode.Joystick5Button16:
break;
case KeyCode.Joystick5Button17:
break;
case KeyCode.Joystick5Button18:
break;
case KeyCode.Joystick5Button19:
break;
case KeyCode.Joystick6Button0:
break;
case KeyCode.Joystick6Button1:
break;
case KeyCode.Joystick6Button2:
break;
case KeyCode.Joystick6Button3:
break;
case KeyCode.Joystick6Button4:
break;
case KeyCode.Joystick6Button5:
break;
case KeyCode.Joystick6Button6:
break;
case KeyCode.Joystick6Button7:
break;
case KeyCode.Joystick6Button8:
break;
case KeyCode.Joystick6Button9:
break;
case KeyCode.Joystick6Button10:
break;
case KeyCode.Joystick6Button11:
break;
case KeyCode.Joystick6Button12:
break;
case KeyCode.Joystick6Button13:
break;
case KeyCode.Joystick6Button14:
break;
case KeyCode.Joystick6Button15:
break;
case KeyCode.Joystick6Button16:
break;
case KeyCode.Joystick6Button17:
break;
case KeyCode.Joystick6Button18:
break;
case KeyCode.Joystick6Button19:
break;
case KeyCode.Joystick7Button0:
break;
case KeyCode.Joystick7Button1:
break;
case KeyCode.Joystick7Button2:
break;
case KeyCode.Joystick7Button3:
break;
case KeyCode.Joystick7Button4:
break;
case KeyCode.Joystick7Button5:
break;
case KeyCode.Joystick7Button6:
break;
case KeyCode.Joystick7Button7:
break;
case KeyCode.Joystick7Button8:
break;
case KeyCode.Joystick7Button9:
break;
case KeyCode.Joystick7Button10:
break;
case KeyCode.Joystick7Button11:
break;
case KeyCode.Joystick7Button12:
break;
case KeyCode.Joystick7Button13:
break;
case KeyCode.Joystick7Button14:
break;
case KeyCode.Joystick7Button15:
break;
case KeyCode.Joystick7Button16:
break;
case KeyCode.Joystick7Button17:
break;
case KeyCode.Joystick7Button18:
break;
case KeyCode.Joystick7Button19:
break;
case KeyCode.Joystick8Button0:
break;
case KeyCode.Joystick8Button1:
break;
case KeyCode.Joystick8Button2:
break;
case KeyCode.Joystick8Button3:
break;
case KeyCode.Joystick8Button4:
break;
case KeyCode.Joystick8Button5:
break;
case KeyCode.Joystick8Button6:
break;
case KeyCode.Joystick8Button7:
break;
case KeyCode.Joystick8Button8:
break;
case KeyCode.Joystick8Button9:
break;
case KeyCode.Joystick8Button10:
break;
case KeyCode.Joystick8Button11:
break;
case KeyCode.Joystick8Button12:
break;
case KeyCode.Joystick8Button13:
break;
case KeyCode.Joystick8Button14:
break;
case KeyCode.Joystick8Button15:
break;
case KeyCode.Joystick8Button16:
break;
case KeyCode.Joystick8Button17:
break;
case KeyCode.Joystick8Button18:
break;
case KeyCode.Joystick8Button19:
break;
//default:
// throw new ArgumentOutOfRangeException(nameof(key), key, null);
}
if (!silent)
Logging.Logger.Log(LogLevel.Debug, $"[{nameof(NewInputSystem)}] Unsupported key: {key}");
return null;
}
}
internal class LegacyInputSystem : IInputSystem
{
public bool GetKey(string name) => Input.GetKey(name);
public bool GetKey(KeyCode key) => Input.GetKey(key);
public bool GetKeyDown(string name) => Input.GetKeyDown(name);
public bool GetKeyDown(KeyCode key) => Input.GetKeyDown(key);
public bool GetKeyUp(string name) => Input.GetKeyUp(name);
public bool GetKeyUp(KeyCode key) => Input.GetKeyUp(key);
public bool GetMouseButton(int button) => Input.GetMouseButton(button);
public bool GetMouseButtonDown(int button) => Input.GetMouseButtonDown(button);
public bool GetMouseButtonUp(int button) => Input.GetMouseButtonUp(button);
public void ResetInputAxes() => Input.ResetInputAxes();
public Vector3 mousePosition => Input.mousePosition;
public Vector2 mouseScrollDelta => Input.mouseScrollDelta;
public bool mousePresent => Input.mousePresent;
public bool anyKey => Input.anyKey;
public bool anyKeyDown => Input.anyKeyDown;
public IEnumerable<KeyCode> SupportedKeyCodes { get; } = (KeyCode[])Enum.GetValues(typeof(KeyCode));
}
}
@@ -1,4 +1,5 @@
using System;
using System.Globalization;
using System.Runtime.CompilerServices;
using BepInEx.Configuration;
using UnityEngine;
@@ -36,5 +37,47 @@ internal static class UnityTomlTypeConverters
TomlTypeConverter.AddConverter(typeof(Vector3), jsonConverter);
TomlTypeConverter.AddConverter(typeof(Vector4), jsonConverter);
TomlTypeConverter.AddConverter(typeof(Quaternion), jsonConverter);
TomlTypeConverter.AddConverter(typeof(Rect), new TypeConverter
{
ConvertToObject = StringToRect,
ConvertToString = RectToString
});
}
private static object StringToRect(string s, Type type)
{
// JsonUtility doesn't work with Rect on all Unity versions, so parse it manually
Rect result = default(Rect);
if (s == null)
return result;
string cleaned = s.Trim('{', '}').Replace(" ", "");
foreach (string part in cleaned.Split(','))
{
string[] parts = part.Split(':');
if (parts.Length != 2 || !float.TryParse(parts[1], out float value))
continue;
string id = parts[0].Trim('"');
if (id == "x")
result.x = value;
else if (id == "y")
result.y = value;
else if (id == "width" || id == "z")
result.width = value;
else if (id == "height" || id == "w")
result.height = value;
}
return result;
}
private static string RectToString(object o, Type type)
{
Rect rect = (Rect)o;
return string.Format(CultureInfo.InvariantCulture,
"{{ \"x\":{0}, \"y\":{1}, \"width\":{2}, \"height\":{3} }}",
rect.x, rect.y, rect.width, rect.height);
}
}
@@ -11,6 +11,9 @@ target_assembly = BepInEx\core\BepInEx.Unity.IL2CPP.dll
# If true, Unity's output log is redirected to <current folder>\output_log.txt
redirect_output_log = false
# Overrides the default boot.config file path
boot_config_override =
# If enabled, DOORSTOP_DISABLE env var value is ignored
# USE THIS ONLY WHEN ASKED TO OR YOU KNOW WHAT THIS MEANS
ignore_disable_switch = false
@@ -28,10 +31,6 @@ dll_search_path_override =
# If true, Mono debugger server will be enabled
debug_enabled = false
# When debug_enabled is true, this option specifies whether Doorstop should initialize the debugger server
# If you experience crashes when starting the debugger on debug UnityPlayer builds, try setting this to false
debug_start_server = true
# When debug_enabled is true, specifies the address to use for the debugger server
debug_address = 127.0.0.1:10000
@@ -11,6 +11,9 @@ target_assembly = BepInEx\core\BepInEx.Unity.Mono.Preloader.dll
# If true, Unity's output log is redirected to <current folder>\output_log.txt
redirect_output_log = false
# Overrides the default boot.config file path
boot_config_override =
# If enabled, DOORSTOP_DISABLE env var value is ignored
# USE THIS ONLY WHEN ASKED TO OR YOU KNOW WHAT THIS MEANS
ignore_disable_switch = false
@@ -23,15 +26,12 @@ ignore_disable_switch = false
# (e.g. mscorlib is stripped in original game)
# This option causes Mono to seek mscorlib and core libraries from a different folder before Managed
# Original Managed folder is added as a secondary folder in the search path
dll_search_path_override =
# To specify multiple paths, separate them with semicolons (;)
dll_search_path_override = "BepInEx\core"
# If true, Mono debugger server will be enabled
debug_enabled = false
# When debug_enabled is true, this option specifies whether Doorstop should initialize the debugger server
# If you experience crashes when starting the debugger on debug UnityPlayer builds, try setting this to false
debug_start_server = true
# When debug_enabled is true, specifies the address to use for the debugger server
debug_address = 127.0.0.1:10000
+125 -82
View File
@@ -8,8 +8,6 @@
# 1. Via CLI: Run ./run_bepinex.sh <path to game> [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,12 +17,16 @@ executable_name=""
# General Config Options
# Enable Doorstop?
# 0 is false, 1 is true
enabled="1"
# Path to the assembly to load and execute
# NOTE: The entrypoint must be of format `static void Doorstop.Entrypoint.Start()`
target_assembly="BepInEx/core/BepInEx.Unity.IL2CPP.dll"
# Overrides the default boot.config file path
boot_config_override=
# If enabled, DOORSTOP_DISABLE env var value is ignored
# USE THIS ONLY WHEN ASKED TO OR YOU KNOW WHAT THIS MEANS
ignore_disable_switch="0"
@@ -36,15 +38,12 @@ ignore_disable_switch="0"
# (e.g. mscorlib is stripped in original game)
# This option causes Mono to seek mscorlib and core libraries from a different folder before Managed
# Original Managed folder is added as a secondary folder in the search path
# To specify multiple paths, separate them with colons (:)
dll_search_path_override=""
# If 1, Mono debugger server will be enabled
debug_enable="0"
# When debug_enabled is 1, this option specifies whether Doorstop should initialize the debugger server
# If you experience crashes when starting the debugger on debug UnityPlayer builds, try setting this to 0
debug_start_server="0"
# When debug_enabled is 1, specifies the address to use for the debugger server
debug_address="127.0.0.1:10000"
@@ -61,43 +60,43 @@ corlib_dir="dotnet"
################################################################################
# Everything past this point is the actual script
set -e
# 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))
# 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
@@ -108,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")"
@@ -163,7 +168,6 @@ _readlink() {
echo "$link"
}
resolve_executable_path () {
e_path="$(abs_path "$1")"
@@ -173,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"
;;
@@ -187,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
@@ -208,61 +222,81 @@ 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
;;
--doorstop-mono-debug-start-server)
debug_start_server="$(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"
export DOORSTOP_MONO_DEBUG_START_SERVER="$debug_start_server"
export DOORSTOP_MONO_DEBUG_ADDRESS="$debug_address"
export DOORSTOP_MONO_DEBUG_SUSPEND="$debug_suspend"
export DOORSTOP_CLR_RUNTIME_CORECLR_PATH="$coreclr_path.$lib_extension"
@@ -286,5 +320,14 @@ else
export DYLD_INSERT_LIBRARIES="${doorstop_name}:${DYLD_INSERT_LIBRARIES}"
fi
# shellcheck disable=SC2086
exec "$executable_path" $rest_args
if [ -n "${is_apple_silicon}" ]; then
export ARCHPREFERENCE="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
+126 -83
View File
@@ -8,8 +8,6 @@
# 1. Via CLI: Run ./run_bepinex.sh <path to game> [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,12 +17,16 @@ executable_name=""
# General Config Options
# Enable Doorstop?
# 0 is false, 1 is true
enabled="1"
# Path to the assembly to load and execute
# NOTE: The entrypoint must be of format `static void Doorstop.Entrypoint.Start()`
target_assembly="BepInEx/core/BepInEx.Unity.Mono.Preloader.dll"
# Overrides the default boot.config file path
boot_config_override=
# If enabled, DOORSTOP_DISABLE env var value is ignored
# USE THIS ONLY WHEN ASKED TO OR YOU KNOW WHAT THIS MEANS
ignore_disable_switch="0"
@@ -36,15 +38,12 @@ ignore_disable_switch="0"
# (e.g. mscorlib is stripped in original game)
# This option causes Mono to seek mscorlib and core libraries from a different folder before Managed
# Original Managed folder is added as a secondary folder in the search path
dll_search_path_override=""
# To specify multiple paths, separate them with colons (:)
dll_search_path_override="BepInEx/core"
# If 1, Mono debugger server will be enabled
debug_enable="0"
# When debug_enabled is 1, this option specifies whether Doorstop should initialize the debugger server
# If you experience crashes when starting the debugger on debug UnityPlayer builds, try setting this to 0
debug_start_server="0"
# When debug_enabled is 1, specifies the address to use for the debugger server
debug_address="127.0.0.1:10000"
@@ -61,43 +60,43 @@ corlib_dir=""
################################################################################
# Everything past this point is the actual script
set -e
# 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))
# 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
@@ -108,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")"
@@ -163,7 +168,6 @@ _readlink() {
echo "$link"
}
resolve_executable_path () {
e_path="$(abs_path "$1")"
@@ -173,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"
;;
@@ -187,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
@@ -208,61 +222,81 @@ 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
;;
--doorstop-mono-debug-start-server)
debug_start_server="$(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"
export DOORSTOP_MONO_DEBUG_START_SERVER="$debug_start_server"
export DOORSTOP_MONO_DEBUG_ADDRESS="$debug_address"
export DOORSTOP_MONO_DEBUG_SUSPEND="$debug_suspend"
export DOORSTOP_CLR_RUNTIME_CORECLR_PATH="$coreclr_path.$lib_extension"
@@ -286,5 +320,14 @@ else
export DYLD_INSERT_LIBRARIES="${doorstop_name}:${DYLD_INSERT_LIBRARIES}"
fi
# shellcheck disable=SC2086
exec "$executable_path" $rest_args
if [ -n "${is_apple_silicon}" ]; then
export ARCHPREFERENCE="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
+1 -1
View File
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory>
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
</PropertyGroup>
+4 -3
View File
@@ -32,7 +32,7 @@ public class BuildContext : FrostingContext
BleedingEdge
}
public const string DoorstopVersion = "4.0.0";
public const string DoorstopVersion = "4.5.0";
public const string DotnetRuntimeVersion = "6.0.7";
public const string DobbyVersion = "1.0.5";
@@ -253,8 +253,9 @@ public sealed class MakeDistTask : FrostingTask<BuildContext>
if (dist.Engine == "Unity")
{
var doorstopPath =
ctx.CacheDirectory.Combine("doorstop").Combine($"doorstop_{dist.Os}").Combine(dist.Arch);
var doorstopPath = dist.Os == "macos"
? ctx.CacheDirectory.Combine("doorstop").Combine("doorstop_macos").Combine("universal")
: ctx.CacheDirectory.Combine("doorstop").Combine($"doorstop_{dist.Os}").Combine(dist.Arch);
foreach (var filePath in ctx.GetFiles(doorstopPath.Combine($"*.{dist.DllExtension}").FullPath))
ctx.CopyFileToDirectory(filePath, targetDir);
ctx.CopyFileToDirectory(doorstopPath.CombineWithFilePath(".doorstop_version"), targetDir);