Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ jobs:
uses: BUTR/workflows/.github/workflows/release-nexusmods.yml@master
with:
nexusmods_game_id: mountandblade2bannerlord
nexusmods_mod_id: 1
nexusmods_mod_id: 13632226197505
nexusmods_file_group_id: 2649439
mod_filename: "Bannerlord Software Extender (BLSE)"
mod_version: ${{ needs.get-changelog.outputs.mod_version }}
Expand Down
6 changes: 6 additions & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
---------------------------------------------------------------------------------------------------
Version: 1.7.3
Game Versions: v1.0.x,v1.1.x,v1.2.x,v1.3.x,v1.4.x,v1.5.x
* Corrected the .dll resolve logic
---------------------------------------------------------------------------------------------------
Version: 1.7.2
Game Versions: v1.0.x,v1.1.x,v1.2.x,v1.3.x,v1.4.x,v1.5.x
* BETA Release!
* Fixed LauncherEx not passing module list
---------------------------------------------------------------------------------------------------
Version: 1.7.1
Game Versions: v1.0.x,v1.1.x,v1.2.x,v1.3.x,v1.4.x,v1.5.x
* BETA Release!
* Fixed the menu being upside-down in LauncherEx
---------------------------------------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;

namespace Bannerlord.BLSE.Features.AssemblyResolver;

internal static class AssemblyResolverCore
{
private const string ModulesMarker = "_MODULES_";

public static Assembly? Resolve(AssemblyName requested, IEnumerable<string> moduleDirectories)
{
if (requested.Name is null) return null;

var loaded = AppDomain.CurrentDomain.GetAssemblies()
.Select(x => (Assembly: x, Name: x.GetName()))
.Where(x => IsSameAssembly(requested, x.Name))
.OrderByDescending(x => IsRequestedVersion(requested, x.Name))
.ThenByDescending(x => x.Name.Version)
.FirstOrDefault();
if (loaded.Assembly is not null) return loaded.Assembly;

var file = GetProbingDirectories().Concat(moduleDirectories)
.Where(x => !string.IsNullOrEmpty(x))
.Select(x => Path.Combine(x, $"{requested.Name}.dll"))
.Where(File.Exists)
.Select(x => (Path: x, Name: TryGetAssemblyName(x)!))
.Where(x => x.Name is not null && IsSameAssembly(requested, x.Name))
.OrderByDescending(x => IsRequestedVersion(requested, x.Name))
.ThenByDescending(x => requested.Version is null || x.Name.Version >= requested.Version)
.FirstOrDefault();
return file.Path is not null ? Assembly.LoadFrom(file.Path) : null;
}

public static string[]? ParseModuleIds(IEnumerable<string?>? args)
{
if (args is null) return null;

var commandLine = string.Join(" ", args.Where(x => x is not null));
var start = commandLine.IndexOf($"{ModulesMarker}*", StringComparison.Ordinal);
if (start < 0) return null;
start += ModulesMarker.Length + 1;

var end = commandLine.IndexOf($"*{ModulesMarker}", start - 1, StringComparison.Ordinal);
if (end < start) return null;

var ids = commandLine.Substring(start, end - start).Split('*').Where(x => x.Length > 0).ToArray();
return ids.Length > 0 ? ids : null;
}

private static IEnumerable<string> GetProbingDirectories()
{
var domain = AppDomain.CurrentDomain;
yield return domain.BaseDirectory;

if (domain.RelativeSearchPath is not { } privateBinPaths) yield break;
foreach (var privateBinPath in privateBinPaths.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries))
yield return Path.Combine(domain.BaseDirectory, privateBinPath);
}

private static AssemblyName? TryGetAssemblyName(string path)
{
try
{
return AssemblyName.GetAssemblyName(path);
}
catch (Exception)
{
// Not a managed assembly, or unreadable
return null;
}
}

/// <summary>The same name and culture. Versions of it are interchangeable here, cultures are not.</summary>
private static bool IsSameAssembly(AssemblyName requested, AssemblyName definition) =>
string.Equals(requested.Name, definition.Name, StringComparison.OrdinalIgnoreCase) &&
(requested.CultureName is null || string.Equals(requested.CultureName, definition.CultureName ?? string.Empty, StringComparison.OrdinalIgnoreCase));

/// <summary>The requested version and public key token. A field the request leaves unspecified matches anything.</summary>
private static bool IsRequestedVersion(AssemblyName requested, AssemblyName definition) =>
(requested.Version is null || requested.Version == definition.Version) &&
(requested.GetPublicKeyToken() is not { } token || token.SequenceEqual(definition.GetPublicKeyToken() ?? Array.Empty<byte>()));
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
using Bannerlord.BLSE.Utils;
using Bannerlord.BLSE.Features.AssemblyResolver.Patches;
using Bannerlord.BLSE.Utils;
using Bannerlord.BUTR.Shared.Helpers;

using HarmonyLib;
using HarmonyLib.BUTR.Extensions;

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
Expand All @@ -20,58 +22,65 @@ public static class AssemblyResolverFeature

public static string Id = FeatureIds.AssemblyResolverId;

private const string HarmonyModuleId = "Bannerlord.Harmony";

private static string[]? _gameArgumentsModuleIds;

public static void Enable(Harmony harmony)
{
AssemblyLoader.Initialize();
AppDomain.CurrentDomain.AssemblyResolve -= AssemblyLoaderOnAssemblyResolve;
AppDomain.CurrentDomain.AssemblyResolve += OnAssemblyResolve;

ProgramPatch.OnMain += args => _gameArgumentsModuleIds = AssemblyResolverCore.ParseModuleIds(args);
ProgramPatch.Enable(harmony);
}

private static Assembly? OnAssemblyResolve(object? sender, ResolveEventArgs args)
{
if (args.Name is null) return null;

var name = new AssemblyName(args.Name);
if (name.Name is null) return null;

foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
if (assembly.FullName == name.FullName)
{
return assembly;
}
}

try
{
var configName = Path.GetFileName(Directory.GetCurrentDirectory());

var isInGame = GameUtils.GetModulesNames() is not null;

var assemblies = (isInGame ? ModuleInfoHelper.GetLoadedModules() : ModuleInfoHelper.GetModules()).Select(x =>
{
var directory = Path.Combine(x.Path, "bin", configName);
return Directory.Exists(directory) ? Directory.GetFiles(directory, "*.dll") : Array.Empty<string>();
}).ToArray();

var assembly = assemblies.SelectMany(x => x).FirstOrDefault(x => Path.GetFileNameWithoutExtension(x) == name.Name);
var name = new AssemblyName(args.Name);
if (name.Name is null) return null;

if (assembly is not null)
{
return Assembly.LoadFrom(assembly);
}

assembly = Directory.GetFiles(Directory.GetCurrentDirectory(), "*.dll").FirstOrDefault(x => Path.GetFileNameWithoutExtension(x) == name.Name);
if (assembly is not null)
{
return Assembly.LoadFrom(assembly);
}
// The directories are enumerated lazily, so a request answered by a loaded assembly doesn't touch the modules
return AssemblyResolverCore.Resolve(name, GetModuleDirectories());
}
catch (Exception)
{
return null;
}
}

/// <summary>
/// The bin directories of the enabled modules, in load order. Within the same match quality the first wins.
/// </summary>
private static IEnumerable<string> GetModuleDirectories()
{
var configName = Path.GetFileName(Directory.GetCurrentDirectory());

// Without a module list, which is the case in the launcher, no module is enabled yet. Nothing may be
// loaded from Modules then, except Harmony, which BLSE itself needs. Whatever is loaded in the launcher
// stays loaded in the game when both run in the same process.
var moduleIds = GetEnabledModuleIds() ?? [HarmonyModuleId];

var modules = ModuleInfoHelper.GetModules().ToDictionary(x => x.Id, StringComparer.OrdinalIgnoreCase);
foreach (var moduleId in moduleIds)
{
if (modules.TryGetValue(moduleId, out var module))
yield return Path.Combine(module.Path, "bin", configName);
}
}

return null;
/// <summary>
/// The engine has the module list once it's up. Before that, the arguments the game was started with have it.
/// Null in the launcher, where nothing is enabled yet.
/// </summary>
private static string[]? GetEnabledModuleIds()
{
var engineModuleIds = GameUtils.GetModulesNames()?.Where(x => x.Length > 0).ToArray();
return engineModuleIds is { Length: > 0 } ? engineModuleIds : _gameArgumentsModuleIds;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using HarmonyLib;
using HarmonyLib.BUTR.Extensions;

using System;

namespace Bannerlord.BLSE.Features.AssemblyResolver.Patches;

/// <summary>
/// Reports the arguments the game is started with. Every loader starts the game through
/// TaleWorlds.Starter.Library.Program.Main, the vanilla launcher included, so this sees the final module list.
/// </summary>
internal static class ProgramPatch
{
public static event Action<string[]>? OnMain;

public static bool Enable(Harmony harmony) => harmony.TryPatch(
AccessTools2.DeclaredMethod("TaleWorlds.Starter.Library.Program:Main"),
prefix: AccessTools2.DeclaredMethod(typeof(ProgramPatch), nameof(MainPrefix)));

private static void MainPrefix(object[] __args)
{
if (__args.Length > 0 && __args[0] is string[] args)
OnMain?.Invoke(args);
}
}
Loading
Loading