Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
88b4838
Fix assembly preloading overriding preloader patches (#1212)
ManlyMarco Nov 14, 2025
dd0655f
Cache downloaded base libraries in the `unity-libs` directory (#1213)
ManlyMarco Nov 16, 2025
0d275a4
Avoid accidentally (indirectly) reading PlatformHelper.Current before…
The0x539 Jan 27, 2026
c038613
Upgrade Doorstop to version 4.5.0 (#1260)
arrowmaster Feb 24, 2026
3fab71a
Improved support for IL2CPP metadata v23-106 (Unity 6+) (#1284)
toebeann Mar 7, 2026
f8ba0a4
Give actionable error when base-libs download fails (#1332)
HetCreep Jun 8, 2026
77d438c
Clarify BepInDependency version errors and docs (#1330)
HetCreep Jun 8, 2026
f568f0e
Fix misleading error label for Finished handlers (#1329)
HetCreep Jun 8, 2026
9aedb90
Set the console codepage to ensure correct text display regardless of…
HayashiUme Jun 9, 2026
a1afbfb
Skip missing search directories to fix preloader DirectoryNotFoundExc…
HetCreep Jun 11, 2026
a06688e
Degrade instead of crashing when the IL2CPP interop assemblies are mi…
HetCreep Jun 11, 2026
bd467c9
Expose a DisplayBepInExVersion (#556)
js6pak Jun 11, 2026
da64b22
Do not abort startup when the config file cannot be written (#1340)
HetCreep Jun 11, 2026
5f39645
Allow multiple [TargetType] and [TargetAssembly] attributes (#1343)
HetCreep Jun 11, 2026
32fb12f
Remove the duplicate byte TomlTypeConverter registration (#1359)
HetCreep Jun 21, 2026
fb5f191
Enumerate a config snapshot under the lock (#1357)
HetCreep Jun 21, 2026
4a879f5
Hash the full UTF-8 byte length in HashStrings (#1355)
HetCreep Jun 21, 2026
4aab292
Make UnityInput and KeyboardShortcut available in IL2CPP (#998)
ManlyMarco Jun 21, 2026
0643c2b
Bump Il2CppInterop to 1.5.3 (#1361)
ManlyMarco Jun 21, 2026
80dde08
Bump Cpp2IL to 2022.1.0.21 and AssetRipper.Primitives to 3.2.0 (#1362)
ManlyMarco Jun 21, 2026
ac4f64a
comment_pr.yml - Fix not working on PRs from forks
ManlyMarco Jun 21, 2026
c58c42d
build_pr.yml - Also run on v5-lts branch
ManlyMarco Jun 21, 2026
0523d6f
Downgrade Samboy063.Cpp2IL.Core to 2022.1.0-pre-release.20
ManlyMarco Jun 24, 2026
6abdba4
Revert Cpp2IL back to development.1452 because of regressions
ManlyMarco Jun 28, 2026
b237b4f
chore: merge upstream master
hazre Aug 10, 2026
4edda32
style: align whitespace with upstream master
hazre Aug 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/build_pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ on:
pull_request:
branches:
- master
- v5-lts

jobs:
build:
Expand Down
23 changes: 21 additions & 2 deletions .github/workflows/comment_pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,28 @@ jobs:
const {owner, repo} = context.repo;
const run_id = ${{github.event.workflow_run.id}};

const pull_requests = ${{ toJSON(github.event.workflow_run.pull_requests) }};
let pull_requests = ${{ toJSON(github.event.workflow_run.pull_requests) }};

if (!pull_requests.length) {
core.info("No PRs in workflow_run payload; resolving by branch head");
const headBranch = context.payload.workflow_run.head_branch;

// Search open PRs in base repo and match by branch name (works for many fork cases)
const { data } = await github.rest.pulls.list({
owner,
repo,
state: "open",
per_page: 100,
});

pull_requests = data.filter(pr => {
// pr.head.ref is branch name; for forks also check same branch name
return pr.head.ref === headBranch;
});
}

if (!pull_requests.length) {
return core.error("This workflow doesn't match any pull requests!");
return core.error("Could not resolve pull request for this workflow run.");
}

const artifacts = await github.paginate(
Expand Down
50 changes: 39 additions & 11 deletions BepInEx.Core/Bootstrap/BaseChainloader.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
Expand All @@ -12,10 +12,10 @@

namespace BepInEx.Bootstrap;

public abstract class BaseChainloader<TPlugin>

Check warning on line 15 in BepInEx.Core/Bootstrap/BaseChainloader.cs

View workflow job for this annotation

GitHub Actions / build

Missing XML comment for publicly visible type or member 'BaseChainloader<TPlugin>'

Check warning on line 15 in BepInEx.Core/Bootstrap/BaseChainloader.cs

View workflow job for this annotation

GitHub Actions / build

Missing XML comment for publicly visible type or member 'BaseChainloader<TPlugin>'
{
protected static readonly string CurrentAssemblyName = Assembly.GetExecutingAssembly().GetName().Name;

Check warning on line 17 in BepInEx.Core/Bootstrap/BaseChainloader.cs

View workflow job for this annotation

GitHub Actions / build

Missing XML comment for publicly visible type or member 'BaseChainloader<TPlugin>.CurrentAssemblyName'

Check warning on line 17 in BepInEx.Core/Bootstrap/BaseChainloader.cs

View workflow job for this annotation

GitHub Actions / build

Missing XML comment for publicly visible type or member 'BaseChainloader<TPlugin>.CurrentAssemblyName'
protected static readonly Version CurrentAssemblyVersion = Assembly.GetExecutingAssembly().GetName().Version;

Check warning on line 18 in BepInEx.Core/Bootstrap/BaseChainloader.cs

View workflow job for this annotation

GitHub Actions / build

Missing XML comment for publicly visible type or member 'BaseChainloader<TPlugin>.CurrentAssemblyVersion'

Check warning on line 18 in BepInEx.Core/Bootstrap/BaseChainloader.cs

View workflow job for this annotation

GitHub Actions / build

Missing XML comment for publicly visible type or member 'BaseChainloader<TPlugin>.CurrentAssemblyVersion'
private static readonly Dictionary<string, bool> RefCache = new();
private static Regex allowedGuidRegex { get; } = new(@"^[a-zA-Z0-9\._\-]+$");

Expand Down Expand Up @@ -107,7 +107,7 @@
}
return RefCache[key] = false;
}
protected static bool HasBepinPlugins(AssemblyDefinition ass)

Check warning on line 110 in BepInEx.Core/Bootstrap/BaseChainloader.cs

View workflow job for this annotation

GitHub Actions / build

Missing XML comment for publicly visible type or member 'BaseChainloader<TPlugin>.HasBepinPlugins(AssemblyDefinition)'

Check warning on line 110 in BepInEx.Core/Bootstrap/BaseChainloader.cs

View workflow job for this annotation

GitHub Actions / build

Missing XML comment for publicly visible type or member 'BaseChainloader<TPlugin>.HasBepinPlugins(AssemblyDefinition)'
{
if (!ReferencesThisAssembly(ass))
return false;
Expand Down Expand Up @@ -370,7 +370,15 @@
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)
{
Expand Down Expand Up @@ -399,6 +407,7 @@
{
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) =>
Expand All @@ -414,15 +423,22 @@
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))
{
Expand All @@ -442,13 +458,25 @@
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;
Expand Down
31 changes: 26 additions & 5 deletions BepInEx.Core/Configuration/ConfigFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public ConfigFile(string configPath, bool saveOnInit, BepInPlugin ownerMetadata)

if (File.Exists(ConfigFilePath))
Reload();
else if (saveOnInit) Save();
else if (saveOnInit) TrySave();
}

public static ConfigFile CoreConfig { get; } = new(Paths.BepInExConfigPath, true);
Expand Down Expand Up @@ -92,9 +92,12 @@ public ConfigEntryBase this[ConfigDefinition key]
public ConfigEntryBase this[string section, string key] => this[new ConfigDefinition(section, key)];

/// <inheritdoc />
public IEnumerator<KeyValuePair<ConfigDefinition, ConfigEntryBase>> GetEnumerator() =>
// We can't really do a read lock for this
Entries.GetEnumerator();
public IEnumerator<KeyValuePair<ConfigDefinition, ConfigEntryBase>> GetEnumerator()
{
// Enumerate a snapshot taken under the lock so a concurrent mutation cannot invalidate the iterator.
lock (_ioLock)
return Entries.ToList().GetEnumerator();
}

IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

Expand Down Expand Up @@ -352,6 +355,24 @@ public void Save()
}
}

/// <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
Expand Down Expand Up @@ -452,7 +473,7 @@ public ConfigEntry<T> Bind<T>(ConfigDefinition configDefinition,
}

if (SaveOnConfigSet)
Save();
TrySave();

return entry;
}
Expand Down
5 changes: 0 additions & 5 deletions BepInEx.Core/Configuration/TomlTypeConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,6 @@ public static class TomlTypeConverter
ConvertToString = (obj, type) => obj.ToString().ToLowerInvariant(),
ConvertToObject = (str, type) => bool.Parse(str)
},
[typeof(byte)] = new TypeConverter
{
ConvertToString = (obj, type) => obj.ToString(),
ConvertToObject = (str, type) => byte.Parse(str)
},

//integral types

Expand Down
2 changes: 1 addition & 1 deletion BepInEx.Core/Console/Unix/ConsoleWriter.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.IO;
using System.Linq;
using System.Reflection;
Expand Down
2 changes: 1 addition & 1 deletion BepInEx.Core/Console/Unix/TtyHandler.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Sections of this code have been abridged from https://github.com/mono/mono/blob/master/mcs/class/corlib/System/TermInfoReader.cs under the MIT license
// Sections of this code have been abridged from https://github.com/mono/mono/blob/master/mcs/class/corlib/System/TermInfoReader.cs under the MIT license

using System;
using System.IO;
Expand Down
2 changes: 1 addition & 1 deletion BepInEx.Core/Console/Unix/UnixStream.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.IO;
using System.Runtime.InteropServices;

Expand Down
2 changes: 1 addition & 1 deletion BepInEx.Core/Console/Unix/UnixStreamHelper.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.IO;
using System.Runtime.InteropServices;
using BepInEx.Core;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public static uint ConsoleCodePage
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;
}

Expand All @@ -50,7 +50,7 @@ public override int GetBytes(char[] chars, int charIndex, int charCount, byte[]
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);
Expand All @@ -60,7 +60,7 @@ public override int GetBytes(char[] chars, int charIndex, int charCount, byte[]
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;
}

Expand All @@ -69,7 +69,7 @@ public override int GetChars(byte[] bytes, int byteIndex, int byteCount, char[]
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;
Expand Down
2 changes: 1 addition & 1 deletion BepInEx.Core/Console/Windows/ConsoleWindow.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// --------------------------------------------------
// --------------------------------------------------
// UnityInjector - ConsoleWindow.cs
// Copyright (c) Usagirei 2015 - 2015
// --------------------------------------------------
Expand Down
8 changes: 5 additions & 3 deletions BepInEx.Core/Console/Windows/WindowsConsoleDriver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,9 @@ public void CreateConsole(uint codepage)
// 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?
Expand Down Expand Up @@ -161,7 +162,7 @@ private static Stream OpenFileStream(IntPtr handle)
{
var windowsConsoleStreamType = Type.GetType("System.ConsolePal+WindowsConsoleStream, System.Console", true);
var constructor = AccessTools.Constructor(windowsConsoleStreamType,
new[] { typeof(IntPtr), typeof(FileAccess), typeof(bool) });
new[] { typeof(IntPtr), typeof(FileAccess), typeof(bool) });
return (Stream)constructor.Invoke(new object[] { handle, FileAccess.Write, true });
}

Expand Down Expand Up @@ -191,3 +192,4 @@ private IntPtr GetOutHandle()
}
}
}

19 changes: 15 additions & 4 deletions BepInEx.Core/Contract/Attributes.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
Expand Down Expand Up @@ -120,8 +120,18 @@ public BepInDependency(string DependencyGUID, DependencyFlags Flags = Dependency
/// 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);
Expand Down Expand Up @@ -266,7 +276,7 @@ internal static IEnumerable<CustomAttribute> GetCustomAttributes<T>(TypeDefiniti
var currentType = td;

do
{
{
result.AddRange(currentType.CustomAttributes.Where(inheritAttribute ? (ca => TypeInheretsFrom(ca.AttributeType, type)) : (ca => ca.AttributeType.FullName == type.FullName)));
currentType = currentType.BaseType?.Resolve();
} while (inheritType && currentType?.FullName != "System.Object");
Expand Down Expand Up @@ -343,3 +353,4 @@ public static IEnumerable<BepInDependency> GetDependencies(Type plugin) =>
}

#endregion

15 changes: 10 additions & 5 deletions BepInEx.Core/PlatformUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ internal static class PlatformUtils
public static Platform Current { get; private set; }

public static bool Is(Platform expected) => (Current & expected) == expected;
public static bool Is(this Platform current, Platform expected) => (current & expected) == expected;
private static bool Is(this Platform current, Platform expected) => (current & expected) == expected;


public static T AsDelegate<T>(this IntPtr procAddress) where T : Delegate
Expand Down Expand Up @@ -140,9 +140,9 @@ public static void SetPlatform()
var windowsVersionInfo = new WindowsOSVersionInfoExW();
RtlGetVersion(ref windowsVersionInfo);

WindowsVersion = new Version((int)windowsVersionInfo.dwMajorVersion,
(int)windowsVersionInfo.dwMinorVersion, 0,
(int)windowsVersionInfo.dwBuildNumber);
WindowsVersion = new Version((int) windowsVersionInfo.dwMajorVersion,
(int) windowsVersionInfo.dwMinorVersion, 0,
(int) windowsVersionInfo.dwBuildNumber);

var ntDll = LoadLibrary("ntdll.dll");
if (ntDll != IntPtr.Zero)
Expand All @@ -151,7 +151,12 @@ public static void SetPlatform()
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();
}
}
Expand Down
Loading
Loading