diff --git a/README.md b/README.md index fa2dc94..72a7c97 100644 --- a/README.md +++ b/README.md @@ -59,8 +59,41 @@ Can be opted-out via passing **/nounblock** in command-line args. Can opted-in by passing **/unblock** in command-line args. * **Continue Save File** - Allows to specify the save file to load when launching the game. Can be used by passing **/continuesave _mysavegame_** in command-line args. +* **Game Pass PC** - Support of modding on the Xbox platform. BLSE disabled Xbox integration, replacing Cloud Saves with saves stored like on Steam/GOG/Epic * **Assembly Resolver** - Changes the game's assembly loading priority. If an assembly is available in one of the loaded modules, it will be loaded from there instead, even if the assembly is available in the main **/bin** folder. * **Interceptor** - BLSE checks if the is a class with a custom attribute named ***BLSEInterceptorAttribute***. If it's found it checks if there are the following signatures: * **static void OnInitializeSubModulesPrefix()** - will execute just before the game starts to initialize the SubModules. This gives us the ability to add SubModules declared in other programming languages like [Python](https://github.com/BUTR/Bannerlord.Python) and [Lua](https://github.com/BUTR/Bannerlord.Lua) * **static void OnLoadSubModulesPostfix()** - will execute just after all SubModules were initialized + +## FAQ +* I have issues with the installation! + *
+ Xbox Game Pass PC +

You need to copy content of '/bin/Gaming.Desktop.x64_Shipping_Client' from BLSE to 'Mount & Blade II- Bannerlord/Content/bin/Gaming.Desktop.x64_Shipping_Client'

+ BLSE Installation Path +

You need to copy content of 'Modules/Bannerlord.Harmony' from BLSE to 'Mount & Blade II- Bannerlord/Content/Modules/Bannerlord.Harmony'

+ Bannerlord.Harmony Installation Path +
+ *
+ Steam +

You need to copy content of '/bin/Win64_Shipping_Client' from BLSE to 'Mount & Blade II Bannerlord/bin/Win64_Shipping_Client'

+ BLSE Installation Path +

You need to copy content of 'Modules/Bannerlord.Harmony' from BLSE to 'Mount & Blade II Bannerlord/Modules/Bannerlord.Harmony'

+ Bannerlord.Harmony Installation Path +
+ *
+ GOG +

You need to copy content of '/bin/Win64_Shipping_Client' from BLSE to 'Mount & Blade II Bannerlord/bin/Win64_Shipping_Client'

+ BLSE Installation Path +

You need to copy content of 'Modules/Bannerlord.Harmony' from BLSE to 'Mount & Blade II Bannerlord/Modules/Bannerlord.Harmony'

+ Bannerlord.Harmony Installation Path +
+* Do I need to include both `Win64_Shipping_Client` and `Gaming.Desktop.x64_Shipping_Client` directories? +No! +For Xbox Game Pass PC you need only `Gaming.Desktop.x64_Shipping_Client` +For Steam/GOG/Epic you need only `Win64_Shipping_Client` +* I don't see my old saves on Xbox Game Pass PC! +BLSE uses a storage that Steam/GOG/Epic versions of the game use. We do not support Xbox's Cloud Saves! +* BLSE is not shown in Vortex's Tools! +You need to add it manually for now! diff --git a/build/common.props b/build/common.props index e534748..d4563b8 100644 --- a/build/common.props +++ b/build/common.props @@ -10,7 +10,7 @@ - 1.2.1 + 1.2.2 2.10.1 3.0.0.135 5.0.198 diff --git a/changelog.txt b/changelog.txt index 9447443..2ec2069 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,4 +1,8 @@ --------------------------------------------------------------------------------------------------- +Version: 1.2.2 +Game Versions: v1.0.0,v1.0.1,v1.0.2,v1.0.3,v1.1.0,v1.1.1 +* Added Steam UAC check +--------------------------------------------------------------------------------------------------- Version: 1.2.1 Game Versions: v1.0.0,v1.0.1,v1.0.2,v1.0.3,v1.1.0,v1.1.1 * Fixed possible GamePass crash diff --git a/src/Bannerlord.BLSE.Shared/NativeMethods.txt b/src/Bannerlord.BLSE.Shared/NativeMethods.txt index aaf9e0a..9f37567 100644 --- a/src/Bannerlord.BLSE.Shared/NativeMethods.txt +++ b/src/Bannerlord.BLSE.Shared/NativeMethods.txt @@ -2,4 +2,6 @@ MessageBox DeleteFile GetConsoleWindow ShowWindow -SetProcessDPIAware \ No newline at end of file +SetProcessDPIAware +OpenProcessToken +GetTokenInformation \ No newline at end of file diff --git a/src/Bannerlord.BLSE.Shared/Program.cs b/src/Bannerlord.BLSE.Shared/Program.cs index 8273aac..ab281d4 100644 --- a/src/Bannerlord.BLSE.Shared/Program.cs +++ b/src/Bannerlord.BLSE.Shared/Program.cs @@ -15,6 +15,9 @@ public static void Main(string[] args) { //PInvoke.ShowWindow(PInvoke.GetConsoleWindow(), SHOW_WINDOW_CMD.SW_HIDE); + if (PlatformHelper.IsSteam()) + UacHelper.CheckSteam(); + LauncherExceptionHandler.Watch(); switch (args[0]) diff --git a/src/Bannerlord.BLSE.Shared/Utils/PlatformHelper.cs b/src/Bannerlord.BLSE.Shared/Utils/PlatformHelper.cs new file mode 100644 index 0000000..605ea28 --- /dev/null +++ b/src/Bannerlord.BLSE.Shared/Utils/PlatformHelper.cs @@ -0,0 +1,16 @@ +using Bannerlord.BUTR.Shared.Helpers; + +using System.IO; + +namespace Bannerlord.BLSE.Shared.Utils; + +internal static class PlatformHelper +{ + private static string ConfigName = Path.GetFileName(Directory.GetCurrentDirectory()); + private static string GameBasePath = Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), "../", "../")); + + public static bool IsSteam() => ConfigName == "Win64_Shipping_Client" && File.Exists(Path.Combine(GameBasePath, ModuleInfoHelper.ModulesFolder, "Native", "steam.target")); + public static bool IsGog() => ConfigName == "Win64_Shipping_Client" && File.Exists(Path.Combine(GameBasePath, ModuleInfoHelper.ModulesFolder, "Native", "gog.target")); + public static bool IsGdk() => ConfigName == "Gaming.Desktop.x64_Shipping_Client" && File.Exists(Path.Combine(GameBasePath, "appxmanifest.xml")); + public static bool IsEpic() => ConfigName == "Win64_Shipping_Client" && File.Exists(Path.Combine(GameBasePath, ModuleInfoHelper.ModulesFolder, "Native", "epic.target")); +} \ No newline at end of file diff --git a/src/Bannerlord.BLSE.Shared/Utils/RegistryHandle.cs b/src/Bannerlord.BLSE.Shared/Utils/RegistryHandle.cs new file mode 100644 index 0000000..f58a9b6 --- /dev/null +++ b/src/Bannerlord.BLSE.Shared/Utils/RegistryHandle.cs @@ -0,0 +1,134 @@ +using Microsoft.Win32.SafeHandles; + +using System; +using System.Runtime.InteropServices; + +namespace Bannerlord.BLSE.Shared.Utils; + +file static class SafeNativeMethods +{ + internal const int ERROR_SUCCESS = 0; + + internal const int KEY_QUERY_VALUE = 1; + internal const int KEY_SET_VALUE = 2; + internal const int KEY_CREATE_SUB_KEY = 4; + internal const int KEY_ENUMERATE_SUB_KEYS = 8; + internal const int KEY_NOTIFY = 16; + internal const int KEY_CREATE_LINK = 32; + internal const int KEY_READ = 131097; + internal const int KEY_WRITE = 131078; + + internal const int REG_NONE = 0; + internal const int REG_SZ = 1; + internal const int REG_EXPAND_SZ = 2; + internal const int REG_BINARY = 3; + internal const int REG_DWORD = 4; + internal const int REG_DWORD_LITTLE_ENDIAN = 4; + internal const int REG_DWORD_BIG_ENDIAN = 5; + internal const int REG_LINK = 6; + internal const int REG_MULTI_SZ = 7; + internal const int REG_RESOURCE_LIST = 8; + internal const int REG_FULL_RESOURCE_DESCRIPTOR = 9; + internal const int REG_RESOURCE_REQUIREMENTS_LIST = 10; + internal const int REG_QWORD = 11; + + internal const string ADVAPI32 = "advapi32.dll"; + + [DllImport(ADVAPI32, BestFitMapping = false, CharSet = CharSet.Unicode)] + internal static extern int RegOpenKeyEx(RegistryHandle hKey, string lpSubKey, int ulOptions, int samDesired, out RegistryHandle? hkResult); + + /* + [DllImport(ADVAPI32, BestFitMapping = false, CharSet = CharSet.Unicode)] + internal static extern int RegSetValueEx(RegistryHandle hKey, string lpValueName, int Reserved, int dwType, string val, int cbData); + + [DllImport(ADVAPI32, BestFitMapping = false, CharSet = CharSet.Unicode)] + internal static extern int RegCreateKeyEx(RegistryHandle hKey, string lpSubKey, int Reserved, string? lpClass, int dwOptions, int samDesigner, IntPtr lpSecurityAttributes, out RegistryHandle hkResult, out int lpdwDisposition); + */ + + [DllImport(ADVAPI32)] + internal static extern int RegCloseKey(IntPtr handle); + + [DllImport(ADVAPI32, BestFitMapping = false, CharSet = CharSet.Unicode)] + internal static extern int RegQueryValueEx(RegistryHandle hKey, string lpValueName, int lpReserved, ref int lpType, [Out] byte[]? lpData, ref int lpcbData); + + /* + [DllImport(ADVAPI32, BestFitMapping = false, CharSet = CharSet.Unicode)] + internal static extern int RegDeleteKey(RegistryHandle hKey, string lpValueName); + */ +} + +internal class RegistryHandle : SafeHandleZeroOrMinusOneIsInvalid +{ + public static readonly RegistryHandle HKEY_CLASSES_ROOT = new(new IntPtr(int.MinValue), ownHandle: false); + public static readonly RegistryHandle HKEY_CURRENT_USER = new(new IntPtr(-2147483647), ownHandle: false); + public static readonly RegistryHandle HKEY_LOCAL_MACHINE = new(new IntPtr(-2147483646), ownHandle: false); + public static readonly RegistryHandle HKEY_USERS = new(new IntPtr(-2147483645), ownHandle: false); + public static readonly RegistryHandle HKEY_PERFORMANCE_DATA = new(new IntPtr(-2147483644), ownHandle: false); + public static readonly RegistryHandle HKEY_CURRENT_CONFIG = new(new IntPtr(-2147483643), ownHandle: false); + public static readonly RegistryHandle HKEY_DYN_DATA = new(new IntPtr(-2147483642), ownHandle: false); + + public static int TryGetHKLMSubkey(string key, out RegistryHandle? regHandle) + { + return SafeNativeMethods.RegOpenKeyEx(HKEY_LOCAL_MACHINE, key, 0, SafeNativeMethods.KEY_READ, out regHandle); + } + + public static RegistryHandle? GetHKLMSubkey(string key) + { + if (SafeNativeMethods.RegOpenKeyEx(HKEY_LOCAL_MACHINE, key, 0, SafeNativeMethods.KEY_READ, out var hkResult) != SafeNativeMethods.ERROR_SUCCESS || hkResult == null || hkResult.IsInvalid) + return null; + return hkResult; + } + + public RegistryHandle(IntPtr hKey, bool ownHandle) : base(ownHandle) => handle = hKey; + public RegistryHandle() : base(ownsHandle: true) { } + + /* + public bool DeleteKey(string key) + { + if (SafeNativeMethods.RegDeleteKey(this, key) == SafeNativeMethods.ERROR_SUCCESS) + { + return true; + } + return false; + } + + public RegistryHandle? CreateSubKey(string subKey) + { + if (SafeNativeMethods.RegCreateKeyEx(this, subKey, 0, null, 0, SafeNativeMethods.KEY_CREATE_SUB_KEY, IntPtr.Zero, out var hkResult, out _) != SafeNativeMethods.ERROR_SUCCESS || hkResult == null || hkResult.IsInvalid) + return null; + return hkResult; + } + + public bool SetValue(string valName, string value) + { + if (SafeNativeMethods.RegSetValueEx(this, valName, 0, SafeNativeMethods.REG_SZ, value, value.Length * 2 + 2) != SafeNativeMethods.ERROR_SUCCESS) + return false; + return true; + } + + public string? GetStringValue(string valName) + { + var lpType = 0; + var lpcbData = 0; + if (SafeNativeMethods.RegQueryValueEx(this, valName, 0, ref lpType, null, ref lpcbData) == SafeNativeMethods.ERROR_SUCCESS && lpType == 1) + { + var array = new byte[lpcbData]; + var num = SafeNativeMethods.RegQueryValueEx(this, valName, 0, ref lpType, array, ref lpcbData); + return new UnicodeEncoding().GetString(array); + } + return null; + } + */ + + public int? GetDwordValue(string valName) + { + var lpType = 4; + var arraySize = 4; + var array = new byte[arraySize]; + if (SafeNativeMethods.RegQueryValueEx(this, valName, 0, ref lpType, array, ref arraySize) == SafeNativeMethods.ERROR_SUCCESS && lpType == 4) + return BitConverter.ToInt32(array, 0); + return null; + } + + protected override bool ReleaseHandle() => SafeNativeMethods.RegCloseKey(handle) == SafeNativeMethods.ERROR_SUCCESS; +} \ No newline at end of file diff --git a/src/Bannerlord.BLSE.Shared/Utils/UacHelper.cs b/src/Bannerlord.BLSE.Shared/Utils/UacHelper.cs new file mode 100644 index 0000000..f7a6bb1 --- /dev/null +++ b/src/Bannerlord.BLSE.Shared/Utils/UacHelper.cs @@ -0,0 +1,88 @@ +using Microsoft.Win32.SafeHandles; + +using System; +using System.Diagnostics; +using System.Linq; + +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.Security; + +namespace Bannerlord.BLSE.Shared.Utils; + +public static class UacHelper +{ + private const string uacRegistryKey = "Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\System"; + private const string uacRegistryValue = "EnableLUA"; + + private static uint STANDARD_RIGHTS_READ = 0x00020000; + private static uint TOKEN_QUERY = 0x0008; + + public enum TOKEN_ELEVATION_TYPE + { + TokenElevationTypeDefault = 1, + TokenElevationTypeFull, + TokenElevationTypeLimited + } + + public static bool IsUacEnabled + { + get + { + using var key = RegistryHandle.GetHKLMSubkey(uacRegistryKey); + return key?.GetDwordValue(uacRegistryValue)?.Equals(1) == true; + } + } + + public static unsafe bool? IsProcessElevated(SafeProcessHandle processHandle) + { + try + { + if (IsUacEnabled) + { + if (!PInvoke.OpenProcessToken(processHandle, TOKEN_ACCESS_MASK.TOKEN_READ, out var tokenHandle) || tokenHandle.IsClosed || tokenHandle.IsInvalid) + return null; + using var __ = tokenHandle; + + var elevationResult = TOKEN_ELEVATION_TYPE.TokenElevationTypeDefault; + const uint elevationResultSize = sizeof(TOKEN_ELEVATION_TYPE); + uint result = 0; + if (PInvoke.GetTokenInformation((HANDLE) tokenHandle.DangerousGetHandle(), TOKEN_INFORMATION_CLASS.TokenElevationType, &elevationResult, elevationResultSize, &result)) + return elevationResult == TOKEN_ELEVATION_TYPE.TokenElevationTypeFull; + + return null; + } + } + catch (Exception) + { + return null; + } + + return false; + } + + public static void CheckSteam() + { + var thisProcess = Process.GetCurrentProcess(); + + var steamProcesses = Process.GetProcessesByName("steam"); + if (steamProcesses.Length != 1) return; + var steamProcess = steamProcesses.First(); + + using var steamProcessHandle = steamProcess.SafeHandle; + var steamElevated = IsProcessElevated(steamProcessHandle); + if (steamElevated is null) return; + + using var thisProcessHandle = thisProcess.SafeHandle; + var thisElevated = IsProcessElevated(thisProcessHandle); + if (thisElevated is null) return; + + if (steamElevated == true && thisElevated != true) + { + MessageBoxDialog.Show(@"Steam is launched as Admin, but BLSE is not! +The game won't work if Steam has higher privileges than the game! +Please run Steam as a user or run the game as Admin!", "Error from BLSE!", MessageBoxButtons.Ok, MessageBoxIcon.Error); + Environment.Exit(1); + } + } +} \ No newline at end of file