From 1a74c5d8a315045fd04b9c0951be48770931c699 Mon Sep 17 00:00:00 2001 From: Al12rs <26797547+Al12rs@users.noreply.github.com> Date: Tue, 22 Oct 2024 15:15:02 +0200 Subject: [PATCH 1/5] Parse bg3 script extender requirements from pak files --- .../Emitters/DependencyDiagnosticEmitter.cs | 25 +- .../BaldursGate3/Pipelines.cs | 11 +- .../Utils/LsxXmlParsing/LsxXmlFormat.cs | 224 +++++++++--------- .../Utils/PakParsing/LspkPackageFormat.cs | 14 ++ .../Utils/PakParsing/PakFileParser.cs | 152 ++++++++++-- .../BG3ModsettingsFileFormatTests.cs | 2 +- .../BaldursGate3/BG3PakParsingTests.cs | 2 +- 7 files changed, 280 insertions(+), 150 deletions(-) diff --git a/src/Games/NexusMods.Games.Larian/BaldursGate3/Emitters/DependencyDiagnosticEmitter.cs b/src/Games/NexusMods.Games.Larian/BaldursGate3/Emitters/DependencyDiagnosticEmitter.cs index 11cf68056b..698d333937 100644 --- a/src/Games/NexusMods.Games.Larian/BaldursGate3/Emitters/DependencyDiagnosticEmitter.cs +++ b/src/Games/NexusMods.Games.Larian/BaldursGate3/Emitters/DependencyDiagnosticEmitter.cs @@ -8,6 +8,7 @@ using NexusMods.Abstractions.Resources; using NexusMods.Abstractions.Telemetry; using NexusMods.Games.Larian.BaldursGate3.Utils.LsxXmlParsing; +using NexusMods.Games.Larian.BaldursGate3.Utils.PakParsing; using NexusMods.Hashing.xxHash64; using Polly; @@ -16,7 +17,7 @@ namespace NexusMods.Games.Larian.BaldursGate3.Emitters; public class DependencyDiagnosticEmitter : ILoadoutDiagnosticEmitter { private readonly ILogger _logger; - private readonly IResourceLoader> _metadataPipeline; + private readonly IResourceLoader> _metadataPipeline; public DependencyDiagnosticEmitter(IServiceProvider serviceProvider, ILogger logger) { @@ -65,7 +66,7 @@ private async Task> DiagnosePakModulesAsync(Loadout.Read // non error case var metadata = metadataOrError.Result; - var dependencies = metadata.Dependencies; + var dependencies = metadata.MetaFileData.Dependencies; foreach (var dependency in dependencies) { @@ -76,7 +77,7 @@ private async Task> DiagnosePakModulesAsync(Loadout.Read var matchingDeps = metaFileTuples.Where( x => x.Item2.Exception is null && - x.Item2.Result.ModuleShortDesc.Uuid == dependencyUuid + x.Item2.Result.MetaFileData.ModuleShortDesc.Uuid == dependencyUuid ) .ToArray(); @@ -87,8 +88,8 @@ x.Item2.Exception is null && ModName: loadoutItemGroup.ToReference(loadout), MissingDepName: dependency.Name, MissingDepVersion: dependency.SemanticVersion.ToString(), - PakModuleName: metadata.ModuleShortDesc.Name, - PakModuleVersion: metadata.ModuleShortDesc.SemanticVersion.ToString(), + PakModuleName: metadata.MetaFileData.ModuleShortDesc.Name, + PakModuleVersion: metadata.MetaFileData.ModuleShortDesc.SemanticVersion.ToString(), NexusModsLink: NexusModsLink ) ); @@ -99,9 +100,9 @@ x.Item2.Exception is null && continue; var highestInstalledMatch = matchingDeps.MaxBy( - x => x.Item2.Result.ModuleShortDesc.SemanticVersion + x => x.Item2.Result.MetaFileData.ModuleShortDesc.SemanticVersion ); - var installedMatchModule = highestInstalledMatch.Item2.Result.ModuleShortDesc; + var installedMatchModule = highestInstalledMatch.Item2.Result.MetaFileData.ModuleShortDesc; var matchLoadoutItemGroup = highestInstalledMatch.Item1.AsLoadoutItemWithTargetPath().AsLoadoutItem().Parent; // Check if found dependency is outdated @@ -109,8 +110,8 @@ x.Item2.Exception is null && { diagnostics.Add(Diagnostics.CreateOutdatedDependency( ModName: loadoutItemGroup.ToReference(loadout), - PakModuleName: metadata.ModuleShortDesc.Name, - PakModuleVersion: metadata.ModuleShortDesc.SemanticVersion.ToString(), + PakModuleName: metadata.MetaFileData.ModuleShortDesc.Name, + PakModuleVersion: metadata.MetaFileData.ModuleShortDesc.SemanticVersion.ToString(), DepModName: matchLoadoutItemGroup.ToReference(loadout), DepName: installedMatchModule.Name, MinDepVersion: dependency.SemanticVersion.ToString(), @@ -128,15 +129,15 @@ x.Item2.Exception is null && #region Helpers - private static async IAsyncEnumerable>> GetAllPakMetadata( + private static async IAsyncEnumerable>> GetAllPakMetadata( LoadoutFile.ReadOnly[] pakLoadoutFiles, - IResourceLoader> metadataPipeline, + IResourceLoader> metadataPipeline, ILogger logger, [EnumeratorCancellation] CancellationToken cancellationToken) { foreach (var pakLoadoutFile in pakLoadoutFiles) { - Resource> resource; + Resource> resource; try { resource = await metadataPipeline.LoadResourceAsync(pakLoadoutFile.Hash, cancellationToken); diff --git a/src/Games/NexusMods.Games.Larian/BaldursGate3/Pipelines.cs b/src/Games/NexusMods.Games.Larian/BaldursGate3/Pipelines.cs index 5adbbe29ed..c6c552a0c0 100644 --- a/src/Games/NexusMods.Games.Larian/BaldursGate3/Pipelines.cs +++ b/src/Games/NexusMods.Games.Larian/BaldursGate3/Pipelines.cs @@ -5,7 +5,6 @@ using NexusMods.Abstractions.Resources; using NexusMods.Abstractions.Resources.Caching; using NexusMods.Abstractions.Resources.IO; -using NexusMods.Games.Larian.BaldursGate3.Utils.LsxXmlParsing; using NexusMods.Games.Larian.BaldursGate3.Utils.PakParsing; using NexusMods.Hashing.xxHash64; using Polly; @@ -18,7 +17,7 @@ public static class Pipelines public static IServiceCollection AddPipelines(this IServiceCollection serviceCollection) { - return serviceCollection.AddKeyedSingleton>>( + return serviceCollection.AddKeyedSingleton>>( serviceKey: MetadataPipelineKey, implementationFactory: static (serviceProvider, _) => CreateMetadataPipeline( fileStore: serviceProvider.GetRequiredService() @@ -26,13 +25,13 @@ public static IServiceCollection AddPipelines(this IServiceCollection serviceCol ); } - public static IResourceLoader> GetMetadataPipeline(IServiceProvider serviceProvider) + public static IResourceLoader> GetMetadataPipeline(IServiceProvider serviceProvider) { return serviceProvider.GetRequiredKeyedService>>(serviceKey: MetadataPipelineKey); + Outcome>>(serviceKey: MetadataPipelineKey); } - private static IResourceLoader> CreateMetadataPipeline(IFileStore fileStore) + private static IResourceLoader> CreateMetadataPipeline(IFileStore fileStore) { // TODO: change pipeline to return C# 9 type unions instead of OneOf var pipeline = new FileStoreStreamLoader(fileStore) @@ -46,7 +45,7 @@ public static IServiceCollection AddPipelines(this IServiceCollection serviceCol } catch (InvalidDataException e) { - return ValueTask.FromResult(resource.WithData(Outcome.FromException(e))); + return ValueTask.FromResult(resource.WithData(Outcome.FromException(e))); } } ) diff --git a/src/Games/NexusMods.Games.Larian/BaldursGate3/Utils/LsxXmlParsing/LsxXmlFormat.cs b/src/Games/NexusMods.Games.Larian/BaldursGate3/Utils/LsxXmlParsing/LsxXmlFormat.cs index 1204dcf48e..d5742aaf9c 100644 --- a/src/Games/NexusMods.Games.Larian/BaldursGate3/Utils/LsxXmlParsing/LsxXmlFormat.cs +++ b/src/Games/NexusMods.Games.Larian/BaldursGate3/Utils/LsxXmlParsing/LsxXmlFormat.cs @@ -7,8 +7,6 @@ namespace NexusMods.Games.Larian.BaldursGate3.Utils.LsxXmlParsing; /// public static class LsxXmlFormat { - - /// /// Pak metadata, containing the module short description and its dependencies. /// @@ -17,8 +15,8 @@ public struct MetaFileData public ModuleShortDesc ModuleShortDesc; public ModuleShortDesc[] Dependencies; } - - + + /// /// Pak module short description. /// @@ -32,15 +30,15 @@ public struct ModuleShortDesc public string Md5; public ModuleVersion SemanticVersion; } - - + + public struct ModuleVersion : IComparable, IEquatable { public ulong Major; public ulong Minor; public ulong Patch; public ulong Build; - + public static ModuleVersion FromInt32String(string? str) { if (string.IsNullOrWhiteSpace(str)) @@ -53,19 +51,20 @@ public static ModuleVersion FromInt32String(string? str) // Apparently the string can contain 64-bit values even though the type is marked as Int32 return FromInt64String(str); } + return FromUInt32(parse32Result); } - + public static ModuleVersion FromInt64String(string? str) { if (string.IsNullOrWhiteSpace(str) || !UInt64.TryParse(str, out var parse64Result)) { return FromUInt64(0); } - + return FromUInt64(parse64Result); } - + private static ModuleVersion FromUInt64(ulong uIntVal) { if (uIntVal == 1 || uIntVal == 268435456) @@ -88,7 +87,7 @@ private static ModuleVersion FromUInt64(ulong uIntVal) }; } - + private static ModuleVersion FromUInt32(UInt32 uIntVal) { if (uIntVal == 1) @@ -101,7 +100,7 @@ private static ModuleVersion FromUInt32(UInt32 uIntVal) Build = 0, }; } - + return new ModuleVersion { Major = uIntVal >> 28, @@ -110,9 +109,9 @@ private static ModuleVersion FromUInt32(UInt32 uIntVal) Build = uIntVal & 0xFFFF, }; } - + public override string ToString() => $"{Major}.{Minor}.{Patch}"; - + public int CompareTo(ModuleVersion other) { var majorComparison = Major.CompareTo(other.Major); @@ -142,7 +141,7 @@ public override int GetHashCode() Build ); } - + public static bool operator !=(ModuleVersion left, ModuleVersion right) => !left.Equals(right); public static bool operator ==(ModuleVersion left, ModuleVersion right) => left.Equals(right); public static bool operator >(ModuleVersion left, ModuleVersion right) => left.CompareTo(right) > 0; @@ -150,8 +149,8 @@ public override int GetHashCode() public static bool operator >=(ModuleVersion left, ModuleVersion right) => left.CompareTo(right) >= 0; public static bool operator <=(ModuleVersion left, ModuleVersion right) => left.CompareTo(right) <= 0; } - - + + /// /// Serializes a to an LSX `ModuleShortDesc` xml element string. /// @@ -162,7 +161,7 @@ public static string SerializeModuleShortDesc(ModuleShortDesc moduleShortDesc) Indent = true, OmitXmlDeclaration = true, }; - + using var stringWriter = new StringWriter(); using (var xmlWriter = XmlWriter.Create(stringWriter, settings)) { @@ -177,102 +176,115 @@ public static string SerializeModuleShortDesc(ModuleShortDesc moduleShortDesc) /// Collection of modules that should be ignored when parsing dependencies. /// These are mostly vanilla pak files, so not relevant for dependencies between mods. /// - public static readonly ModuleShortDesc[] DependencyModulesToIgnore = + public static readonly ModuleShortDesc[] DependencyModulesToIgnore = + [ - new() - { - // Vanilla pak file - Name = "Gustav", - Uuid = "991c9c7a-fb80-40cb-8f0d-b92d4e80e9b1", - }, - new() - { - // Vanilla pak file - Name = "DiceSet_01", - Uuid = "e842840a-2449-588c-b0c4-22122cfce31b", - }, - new() - { - // Vanilla pak file - Name = "GustavDev", - Uuid = "28ac9ce2-2aba-8cda-b3b5-6e922f71b6b8", - }, - new() - { - // Vanilla pak file - Name = "DiceSet_02", - Uuid = "b176a0ac-d79f-ed9d-5a87-5c2c80874e10", - }, - new() - { - // Vanilla pak file - Name = "DiceSet_03", - Uuid = "e0a4d990-7b9b-8fa9-d7c6-04017c6cf5b1", - }, - new() - { - // Vanilla pak file - Name = "DiceSet_04", - Uuid = "77a2155f-4b35-4f0c-e7ff-4338f91426a4", - }, - new() - { - // Vanilla pak file - Name = "Shared", - Uuid = "ed539163-bb70-431b-96a7-f5b2eda5376b", - }, - new() - { - // Vanilla pak file - Name = "SharedDev", - Uuid = "3d0c5ff8-c95d-c907-ff3e-34b204f1c630", - }, - new() - { - // Vanilla pak file - Name = "FW3", - Uuid = "e5c9077e-1fca-4f24-b55d-464f512c98a8", - }, - new() - { - // Vanilla pak file - Name = "Engine", - Uuid = "9dff4c3b-fda7-43de-a763-ce1383039999", - }, - new() - { - // Vanilla pak file - Name = "DiceSet_06", - Uuid = "ee4989eb-aab8-968f-8674-812ea2f4bfd7", - }, - new() - { - // Vanilla pak file - Name = "Honour", - Uuid = "b77b6210-ac50-4cb1-a3d5-5702fb9c744c", - }, - new() - { - // Vanilla pak file - Name = "ModBrowser", - Uuid = "ee5a55ff-eb38-0b27-c5b0-f358dc306d34", - }, - new() - { - // Vanilla pak file - Name = "MainUI", - Uuid = "630daa32-70f8-3da5-41b9-154fe8410236", - }, + new () + { + // Vanilla pak file + Name = "Gustav", + Uuid = "991c9c7a-fb80-40cb-8f0d-b92d4e80e9b1", + }, + + new () + { + // Vanilla pak file + Name = "DiceSet_01", + Uuid = "e842840a-2449-588c-b0c4-22122cfce31b", + }, + + new () + { + // Vanilla pak file + Name = "GustavDev", + Uuid = "28ac9ce2-2aba-8cda-b3b5-6e922f71b6b8", + }, + + new () + { + // Vanilla pak file + Name = "DiceSet_02", + Uuid = "b176a0ac-d79f-ed9d-5a87-5c2c80874e10", + }, + + new () + { + // Vanilla pak file + Name = "DiceSet_03", + Uuid = "e0a4d990-7b9b-8fa9-d7c6-04017c6cf5b1", + }, + + new () + { + // Vanilla pak file + Name = "DiceSet_04", + Uuid = "77a2155f-4b35-4f0c-e7ff-4338f91426a4", + }, + + new () + { + // Vanilla pak file + Name = "Shared", + Uuid = "ed539163-bb70-431b-96a7-f5b2eda5376b", + }, + + new () + { + // Vanilla pak file + Name = "SharedDev", + Uuid = "3d0c5ff8-c95d-c907-ff3e-34b204f1c630", + }, + + new () + { + // Vanilla pak file + Name = "FW3", + Uuid = "e5c9077e-1fca-4f24-b55d-464f512c98a8", + }, + + new () + { + // Vanilla pak file + Name = "Engine", + Uuid = "9dff4c3b-fda7-43de-a763-ce1383039999", + }, + + new () + { + // Vanilla pak file + Name = "DiceSet_06", + Uuid = "ee4989eb-aab8-968f-8674-812ea2f4bfd7", + }, + + new () + { + // Vanilla pak file + Name = "Honour", + Uuid = "b77b6210-ac50-4cb1-a3d5-5702fb9c744c", + }, + + new () + { + // Vanilla pak file + Name = "ModBrowser", + Uuid = "ee5a55ff-eb38-0b27-c5b0-f358dc306d34", + }, + + new () + { + // Vanilla pak file + Name = "MainUI", + Uuid = "630daa32-70f8-3da5-41b9-154fe8410236", + }, ]; - + /// /// Dependency Name values to ignore, based on the collection. /// public static readonly string[] DependencyNamesToIgnore = DependencyModulesToIgnore.Select(m => m.Name).ToArray(); - + /// /// Dependency Uuid values to ignore, based on the collection. /// public static readonly string[] DependencyUuidsToIgnore = DependencyModulesToIgnore.Select(m => m.Uuid).ToArray(); - } diff --git a/src/Games/NexusMods.Games.Larian/BaldursGate3/Utils/PakParsing/LspkPackageFormat.cs b/src/Games/NexusMods.Games.Larian/BaldursGate3/Utils/PakParsing/LspkPackageFormat.cs index 01fae4e076..af05c367c8 100644 --- a/src/Games/NexusMods.Games.Larian/BaldursGate3/Utils/PakParsing/LspkPackageFormat.cs +++ b/src/Games/NexusMods.Games.Larian/BaldursGate3/Utils/PakParsing/LspkPackageFormat.cs @@ -1,4 +1,6 @@ using System.Text; +using DynamicData.Kernel; +using NexusMods.Games.Larian.BaldursGate3.Utils.LsxXmlParsing; namespace NexusMods.Games.Larian.BaldursGate3.Utils.PakParsing; @@ -7,6 +9,18 @@ namespace NexusMods.Games.Larian.BaldursGate3.Utils.PakParsing; /// public static class LspkPackageFormat { + + public struct PakMetaData + { + public LsxXmlFormat.MetaFileData MetaFileData; + public Optional ScriptExtenderConfigMetadata; + } + public struct ScriptExtenderConfigMetadata + { + public bool RequiresScriptExtender; + public int SeRequiredVersion; + } + #region Enums public enum PackageVersion diff --git a/src/Games/NexusMods.Games.Larian/BaldursGate3/Utils/PakParsing/PakFileParser.cs b/src/Games/NexusMods.Games.Larian/BaldursGate3/Utils/PakParsing/PakFileParser.cs index 70061ecbe1..191c91d03d 100644 --- a/src/Games/NexusMods.Games.Larian/BaldursGate3/Utils/PakParsing/PakFileParser.cs +++ b/src/Games/NexusMods.Games.Larian/BaldursGate3/Utils/PakParsing/PakFileParser.cs @@ -1,8 +1,10 @@ using System.IO.Compression; using System.Text; +using System.Text.Json; using DynamicData.Kernel; using K4os.Compression.LZ4; using NexusMods.Games.Larian.BaldursGate3.Utils.LsxXmlParsing; +using NexusMods.Paths; using ZstdSharp; namespace NexusMods.Games.Larian.BaldursGate3.Utils.PakParsing; @@ -13,14 +15,13 @@ namespace NexusMods.Games.Larian.BaldursGate3.Utils.PakParsing; /// public static class PakFileParser { - #region Public Methods /// /// Parses a bg3 `.pak` file stream and extracts the metadata from the packed `meta.lsx` file. /// /// In case of errors during parsing - public static LsxXmlFormat.MetaFileData ParsePakMeta(Stream pakFileStream) + public static LspkPackageFormat.PakMetaData ParsePakMeta(Stream pakFileStream) { using var br = new BinaryReader(pakFileStream); var headerData = ParseHeaderInternal(br); @@ -32,21 +33,52 @@ public static LsxXmlFormat.MetaFileData ParsePakMeta(Stream pakFileStream) throw new InvalidDataException($"Unable to find `meta.lsx` file in pak archive"); } - var metaStream = ReadFileEntryData(br, fileEntryInfo.Value); - return MetaLsxParser.ParseMetaFile(metaStream); - } + var metaStream = GetFileEntryStream(br, fileEntryInfo.Value); + var metaFile = MetaLsxParser.ParseMetaFile(metaStream); -#endregion + var seConfig = GetScriptExtenderConfigMetaData(fileList, br); -#region Private Methods + return new LspkPackageFormat.PakMetaData + { + MetaFileData = metaFile, + ScriptExtenderConfigMetadata = seConfig, + }; + } - private static void Load(BinaryReader br) + private static Optional GetScriptExtenderConfigMetaData(List fileList, BinaryReader br) { - var headerData = ParseHeaderInternal(br); + var seConfig = fileList.FirstOrOptional( + f => new RelativePath(f.Name).EndsWith(new RelativePath("ScriptExtender/Config.json")) + ); - var fileList = ParseFileListInternal(br, (int)headerData.FileListOffset, headerData); + if (seConfig.HasValue) + { + var configStream = GetFileEntryBytes(br, seConfig.Value); + var jsonReader = new Utf8JsonReader(configStream); + while (jsonReader.Read()) + { + if (jsonReader.TokenType != JsonTokenType.PropertyName || jsonReader.GetString() != "RequiredVersion") + continue; + + jsonReader.Read(); + if (jsonReader.TokenType == JsonTokenType.Number && jsonReader.TryGetInt32(out var requiredVersion)) + { + return new LspkPackageFormat.ScriptExtenderConfigMetadata + { + RequiresScriptExtender = true, + SeRequiredVersion = requiredVersion, + }; + } + } + } + + return Optional.None; } +#endregion + +#region Private Methods + private static LspkPackageFormat.HeaderCommon ParseHeaderInternal(BinaryReader br) { var magic = br.ReadBytes(4); @@ -171,8 +203,74 @@ private static LspkPackageFormat.FileEntryInfoCommon ParseFileEntryInternal(Bina throw new InvalidDataException($"Unrecognized Pak version: v{version}"); } } - - private static Stream ReadFileEntryData(BinaryReader br, LspkPackageFormat.FileEntryInfoCommon fileMeta) + + + private static byte[] GetFileEntryBytes(BinaryReader br, LspkPackageFormat.FileEntryInfoCommon fileMeta) + { + br.BaseStream.Seek((long)fileMeta.OffsetInFile, SeekOrigin.Begin); + + var rawData = br.ReadBytes((int)fileMeta.SizeOnDisk); + + return fileMeta.Flags.Method() switch + { + LspkPackageFormat.CompressionMethod.None => rawData, + LspkPackageFormat.CompressionMethod.LZ4 => DecompressLz4(fileMeta, rawData), + LspkPackageFormat.CompressionMethod.Zlib => DecompressZlib(fileMeta, rawData), + LspkPackageFormat.CompressionMethod.Zstd => DecompressZstd(fileMeta, rawData), + _ => throw new InvalidDataException($"Unsupported compression method {fileMeta.Flags.Method()} for file {fileMeta.Name}") + }; + + byte[] DecompressLz4(LspkPackageFormat.FileEntryInfoCommon fileEntryInfoCommon, byte[] bytes) + { + var decompressedBytes = new byte[fileEntryInfoCommon.UncompressedSize]; + + var decodedSize = LZ4Codec.Decode(bytes, 0, bytes.Length, + decompressedBytes, 0, decompressedBytes.Length + ); + if (decodedSize != decompressedBytes.Length) + { + throw new InvalidDataException( + $"Failed to extract {fileEntryInfoCommon.Name} from Pak archive: decompressed size {decodedSize} does not match expected size {fileEntryInfoCommon.UncompressedSize}" + ); + } + + return decompressedBytes; + } + + byte[] DecompressZlib(LspkPackageFormat.FileEntryInfoCommon fileEntryInfoCommon, byte[] bytes) + { + using var ms = new MemoryStream(bytes); + using var ds = new ZLibStream(ms, CompressionMode.Decompress); + var decompressedBytes = new byte[fileEntryInfoCommon.UncompressedSize]; + var read = ds.Read(decompressedBytes, 0, decompressedBytes.Length); + if (read != decompressedBytes.Length) + { + throw new InvalidDataException( + $"Failed to extract {fileEntryInfoCommon.Name} from Pak archive: decompressed size {read} does not match expected size {fileEntryInfoCommon.UncompressedSize}" + ); + } + + return decompressedBytes; + } + + byte[] DecompressZstd(LspkPackageFormat.FileEntryInfoCommon fileEntryInfoCommon, byte[] bytes) + { + using var ms = new MemoryStream(bytes); + using var ds = new DecompressionStream(ms); + var decompressedBytes = new byte[fileEntryInfoCommon.UncompressedSize]; + var read = ds.Read(decompressedBytes, 0, decompressedBytes.Length); + if (read != decompressedBytes.Length) + { + throw new InvalidDataException( + $"Failed to extract {fileEntryInfoCommon.Name} from Pak archive: decompressed size {read} does not match expected size {fileEntryInfoCommon.UncompressedSize}" + ); + } + + return decompressedBytes; + } + } + + private static Stream GetFileEntryStream(BinaryReader br, LspkPackageFormat.FileEntryInfoCommon fileMeta) { br.BaseStream.Seek((long)fileMeta.OffsetInFile, SeekOrigin.Begin); @@ -190,16 +288,20 @@ private static Stream ReadFileEntryData(BinaryReader br, LspkPackageFormat.FileE Stream DecompressLz4(LspkPackageFormat.FileEntryInfoCommon fileEntryInfoCommon, byte[] bytes) { var decompressedBytes = new byte[fileEntryInfoCommon.UncompressedSize]; - - var decodedSize = LZ4Codec.Decode(bytes, 0, bytes.Length, decompressedBytes, 0, decompressedBytes.Length); + + var decodedSize = LZ4Codec.Decode(bytes, 0, bytes.Length, + decompressedBytes, 0, decompressedBytes.Length + ); if (decodedSize != decompressedBytes.Length) { - throw new InvalidDataException($"Failed to extract {fileEntryInfoCommon.Name} from Pak archive: decompressed size {decodedSize} does not match expected size {fileEntryInfoCommon.UncompressedSize}"); + throw new InvalidDataException( + $"Failed to extract {fileEntryInfoCommon.Name} from Pak archive: decompressed size {decodedSize} does not match expected size {fileEntryInfoCommon.UncompressedSize}" + ); } - + return new MemoryStream(decompressedBytes); } - + Stream DecompressZlib(LspkPackageFormat.FileEntryInfoCommon fileEntryInfoCommon, byte[] bytes) { using var ms = new MemoryStream(bytes); @@ -208,12 +310,14 @@ Stream DecompressZlib(LspkPackageFormat.FileEntryInfoCommon fileEntryInfoCommon, var read = ds.Read(decompressedBytes, 0, decompressedBytes.Length); if (read != decompressedBytes.Length) { - throw new InvalidDataException($"Failed to extract {fileEntryInfoCommon.Name} from Pak archive: decompressed size {read} does not match expected size {fileEntryInfoCommon.UncompressedSize}"); + throw new InvalidDataException( + $"Failed to extract {fileEntryInfoCommon.Name} from Pak archive: decompressed size {read} does not match expected size {fileEntryInfoCommon.UncompressedSize}" + ); } - + return new MemoryStream(decompressedBytes); } - + Stream DecompressZstd(LspkPackageFormat.FileEntryInfoCommon fileEntryInfoCommon, byte[] bytes) { using var ms = new MemoryStream(bytes); @@ -222,14 +326,14 @@ Stream DecompressZstd(LspkPackageFormat.FileEntryInfoCommon fileEntryInfoCommon, var read = ds.Read(decompressedBytes, 0, decompressedBytes.Length); if (read != decompressedBytes.Length) { - throw new InvalidDataException($"Failed to extract {fileEntryInfoCommon.Name} from Pak archive: decompressed size {read} does not match expected size {fileEntryInfoCommon.UncompressedSize}"); + throw new InvalidDataException( + $"Failed to extract {fileEntryInfoCommon.Name} from Pak archive: decompressed size {read} does not match expected size {fileEntryInfoCommon.UncompressedSize}" + ); } - + return new MemoryStream(decompressedBytes); } } - - #endregion } diff --git a/tests/Games/NexusMods.Games.Larian.Tests/BaldursGate3/BG3ModsettingsFileFormatTests.cs b/tests/Games/NexusMods.Games.Larian.Tests/BaldursGate3/BG3ModsettingsFileFormatTests.cs index 469a5dd6d0..4f9c8cf65f 100644 --- a/tests/Games/NexusMods.Games.Larian.Tests/BaldursGate3/BG3ModsettingsFileFormatTests.cs +++ b/tests/Games/NexusMods.Games.Larian.Tests/BaldursGate3/BG3ModsettingsFileFormatTests.cs @@ -28,7 +28,7 @@ public async Task SerializeModsettingsLoadOrder_VerifyTest() LsxXmlFormat.MetaFileData metaFileData; try { - metaFileData = PakFileParser.ParsePakMeta(pakFileStream); + metaFileData = PakFileParser.ParsePakMeta(pakFileStream).MetaFileData; } catch (InvalidDataException) { diff --git a/tests/Games/NexusMods.Games.Larian.Tests/BaldursGate3/BG3PakParsingTests.cs b/tests/Games/NexusMods.Games.Larian.Tests/BaldursGate3/BG3PakParsingTests.cs index 2bf0fc3a97..254d61a48e 100644 --- a/tests/Games/NexusMods.Games.Larian.Tests/BaldursGate3/BG3PakParsingTests.cs +++ b/tests/Games/NexusMods.Games.Larian.Tests/BaldursGate3/BG3PakParsingTests.cs @@ -26,7 +26,7 @@ public async Task ParsePakMeta_ShouldParseCorrectly(string pakFilePath) { var fullPath = _fs.GetKnownPath(KnownPath.EntryDirectory).Combine("BaldursGate3/Resources/PakFiles/" + pakFilePath); await using var pakFileStream = File.OpenRead(fullPath.ToString()); - var metaFileData = PakFileParser.ParsePakMeta(pakFileStream); + var metaFileData = PakFileParser.ParsePakMeta(pakFileStream).MetaFileData; var sb = new StringBuilder(); sb.AppendLine("ModuleShortDesc:"); From 9318b9f6600bc9c2741675d1a48498c4045c7e44 Mon Sep 17 00:00:00 2001 From: Al12rs <26797547+Al12rs@users.noreply.github.com> Date: Wed, 23 Oct 2024 10:06:25 +0200 Subject: [PATCH 2/5] WIP: Detect BG3SE --- .../BaldursGate3/BG3Constants.cs | 2 ++ .../Emitters/DependencyDiagnosticEmitter.cs | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/src/Games/NexusMods.Games.Larian/BaldursGate3/BG3Constants.cs b/src/Games/NexusMods.Games.Larian/BaldursGate3/BG3Constants.cs index ef3811fa29..c23ad70fdf 100644 --- a/src/Games/NexusMods.Games.Larian/BaldursGate3/BG3Constants.cs +++ b/src/Games/NexusMods.Games.Larian/BaldursGate3/BG3Constants.cs @@ -8,4 +8,6 @@ public static class Bg3Constants public static readonly Extension PakFileExtension = new(".pak"); public static readonly LocationId ModsLocationId = LocationId.From("Mods"); + + public static readonly GamePath BG3SEGamePath = new(LocationId.Game, "bin/DWrite.dll"); } diff --git a/src/Games/NexusMods.Games.Larian/BaldursGate3/Emitters/DependencyDiagnosticEmitter.cs b/src/Games/NexusMods.Games.Larian/BaldursGate3/Emitters/DependencyDiagnosticEmitter.cs index 698d333937..235b4f8d54 100644 --- a/src/Games/NexusMods.Games.Larian/BaldursGate3/Emitters/DependencyDiagnosticEmitter.cs +++ b/src/Games/NexusMods.Games.Larian/BaldursGate3/Emitters/DependencyDiagnosticEmitter.cs @@ -1,4 +1,5 @@ using System.Runtime.CompilerServices; +using DynamicData.Kernel; using Microsoft.Extensions.Logging; using NexusMods.Abstractions.Diagnostics; using NexusMods.Abstractions.Diagnostics.Emitters; @@ -10,6 +11,9 @@ using NexusMods.Games.Larian.BaldursGate3.Utils.LsxXmlParsing; using NexusMods.Games.Larian.BaldursGate3.Utils.PakParsing; using NexusMods.Hashing.xxHash64; +using NexusMods.MnemonicDB.Abstractions; +using NexusMods.MnemonicDB.Abstractions.IndexSegments; + using Polly; namespace NexusMods.Games.Larian.BaldursGate3.Emitters; @@ -129,6 +133,12 @@ x.Item2.Exception is null && #region Helpers + private static Optional GetScriptExtenderLoadoutFile(Loadout.ReadOnly loadout) + { + var datoms = IndexSegmentExtensions.Datoms(loadout.Db, (LoadoutItem.LoadoutId, loadout.LoadoutId), (LoadoutItemWithTargetPath.TargetPath, (, Bg3Constants.BG3SEGamePath.LocationId, Bg3Constants.BG3SEGamePath.Path) )); + } + + private static async IAsyncEnumerable>> GetAllPakMetadata( LoadoutFile.ReadOnly[] pakLoadoutFiles, IResourceLoader> metadataPipeline, From 6ff0f48eab20be39f670d34f6c0e032f5fb5d52e Mon Sep 17 00:00:00 2001 From: Al12rs <26797547+Al12rs@users.noreply.github.com> Date: Wed, 23 Oct 2024 10:52:58 +0200 Subject: [PATCH 3/5] WIP: find BG3SE mod --- .../Emitters/DependencyDiagnosticEmitter.cs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/Games/NexusMods.Games.Larian/BaldursGate3/Emitters/DependencyDiagnosticEmitter.cs b/src/Games/NexusMods.Games.Larian/BaldursGate3/Emitters/DependencyDiagnosticEmitter.cs index 235b4f8d54..409b2c98c5 100644 --- a/src/Games/NexusMods.Games.Larian/BaldursGate3/Emitters/DependencyDiagnosticEmitter.cs +++ b/src/Games/NexusMods.Games.Larian/BaldursGate3/Emitters/DependencyDiagnosticEmitter.cs @@ -6,14 +6,12 @@ using NexusMods.Abstractions.Diagnostics.References; using NexusMods.Abstractions.Diagnostics.Values; using NexusMods.Abstractions.Loadouts; +using NexusMods.Abstractions.Loadouts.Extensions; using NexusMods.Abstractions.Resources; using NexusMods.Abstractions.Telemetry; using NexusMods.Games.Larian.BaldursGate3.Utils.LsxXmlParsing; using NexusMods.Games.Larian.BaldursGate3.Utils.PakParsing; using NexusMods.Hashing.xxHash64; -using NexusMods.MnemonicDB.Abstractions; -using NexusMods.MnemonicDB.Abstractions.IndexSegments; - using Polly; namespace NexusMods.Games.Larian.BaldursGate3.Emitters; @@ -135,10 +133,17 @@ x.Item2.Exception is null && private static Optional GetScriptExtenderLoadoutFile(Loadout.ReadOnly loadout) { - var datoms = IndexSegmentExtensions.Datoms(loadout.Db, (LoadoutItem.LoadoutId, loadout.LoadoutId), (LoadoutItemWithTargetPath.TargetPath, (, Bg3Constants.BG3SEGamePath.LocationId, Bg3Constants.BG3SEGamePath.Path) )); + return loadout.Items.OfTypeLoadoutItemGroup() + .Where(g => g.AsLoadoutItem().IsEnabled()) + .SelectMany(group => group.Children.OfTypeLoadoutItemWithTargetPath() + .OfTypeLoadoutFile() + .Where(file => file.AsLoadoutItemWithTargetPath().TargetPath.Item2 == Bg3Constants.BG3SEGamePath.LocationId && + file.AsLoadoutItemWithTargetPath().TargetPath.Item3 == Bg3Constants.BG3SEGamePath.Path + ) + ).FirstOrOptional(_ => true); } - - + + private static async IAsyncEnumerable>> GetAllPakMetadata( LoadoutFile.ReadOnly[] pakLoadoutFiles, IResourceLoader> metadataPipeline, From 51091e539125483bc8bec0bffc3460749c111a39 Mon Sep 17 00:00:00 2001 From: Al12rs <26797547+Al12rs@users.noreply.github.com> Date: Wed, 23 Oct 2024 11:57:08 +0200 Subject: [PATCH 4/5] Add diagnostic in case of missing Script Extender and mods requiring it --- .../BaldursGate3/Diagnostics.cs | 25 ++++++++++++++-- .../Emitters/DependencyDiagnosticEmitter.cs | 30 ++++++++++++++----- 2 files changed, 44 insertions(+), 11 deletions(-) diff --git a/src/Games/NexusMods.Games.Larian/BaldursGate3/Diagnostics.cs b/src/Games/NexusMods.Games.Larian/BaldursGate3/Diagnostics.cs index 0ddf47ef33..f384867e8b 100644 --- a/src/Games/NexusMods.Games.Larian/BaldursGate3/Diagnostics.cs +++ b/src/Games/NexusMods.Games.Larian/BaldursGate3/Diagnostics.cs @@ -44,7 +44,7 @@ internal static partial class Diagnostics [UsedImplicitly] internal static IDiagnosticTemplate OutdatedDependencyTemplate = DiagnosticTemplateBuilder .Start() - .WithId(new DiagnosticId(Source, number: 1)) + .WithId(new DiagnosticId(Source, number: 2)) .WithTitle("Required dependency is outdated") .WithSeverity(DiagnosticSeverity.Warning) .WithSummary("Mod {ModName} requires at least version {MinDepVersion}+ of '{DepName}' but only v{CurrentDepVersion} is installed.") @@ -67,7 +67,7 @@ internal static partial class Diagnostics [UsedImplicitly] internal static IDiagnosticTemplate InvalidPakFileTemplate = DiagnosticTemplateBuilder .Start() - .WithId(new DiagnosticId(Source, number: 1)) + .WithId(new DiagnosticId(Source, number: 3)) .WithTitle("Invalid pak file") .WithSeverity(DiagnosticSeverity.Warning) .WithSummary("Invalid .pak File Detected in {ModName}") @@ -84,5 +84,24 @@ internal static partial class Diagnostics .AddValue("PakFileName") ) .Finish(); - + + [DiagnosticTemplate] [UsedImplicitly] internal static IDiagnosticTemplate MissingRequiredScriptExtenderTemplate = DiagnosticTemplateBuilder + .Start() + .WithId(new DiagnosticId(Source, number: 4)) + .WithTitle("Missing Script Extender") + .WithSeverity(DiagnosticSeverity.Warning) + .WithSummary("Missing BG3 Script Extender, required by {ModName}") + .WithDetails(""" + The .pak file {PakName} lists the Baldur's Gate 3 Script Extender (BG3SE) as a dependency, but it isn't installed. + + ## Recommended Actions + Install the BG3 Script Extender from {BG3SENexusLink} or from the official source. + """ + ) + .WithMessageData(messageBuilder => messageBuilder + .AddDataReference("ModName") + .AddValue("PakName") + .AddValue("BG3SENexusLink")) + .Finish(); + } diff --git a/src/Games/NexusMods.Games.Larian/BaldursGate3/Emitters/DependencyDiagnosticEmitter.cs b/src/Games/NexusMods.Games.Larian/BaldursGate3/Emitters/DependencyDiagnosticEmitter.cs index 409b2c98c5..e9e16f362a 100644 --- a/src/Games/NexusMods.Games.Larian/BaldursGate3/Emitters/DependencyDiagnosticEmitter.cs +++ b/src/Games/NexusMods.Games.Larian/BaldursGate3/Emitters/DependencyDiagnosticEmitter.cs @@ -29,7 +29,8 @@ public DependencyDiagnosticEmitter(IServiceProvider serviceProvider, ILogger Diagnose(Loadout.ReadOnly loadout, [EnumeratorCancellation] CancellationToken cancellationToken) { - var diagnostics = await DiagnosePakModulesAsync(loadout, cancellationToken); + var bg3LoadoutFile = GetScriptExtenderLoadoutFile(loadout); + var diagnostics = await DiagnosePakModulesAsync(loadout, bg3LoadoutFile, cancellationToken); foreach (var diagnostic in diagnostics) { yield return diagnostic; @@ -38,7 +39,7 @@ public async IAsyncEnumerable Diagnose(Loadout.ReadOnly loadout, [En #region Diagnosers - private async Task> DiagnosePakModulesAsync(Loadout.ReadOnly loadout, CancellationToken cancellationToken) + private async Task> DiagnosePakModulesAsync(Loadout.ReadOnly loadout, Optional bg3LoadoutFile, CancellationToken cancellationToken) { var pakLoadoutFiles = GetAllPakLoadoutFiles(loadout, onlyEnabledMods: true); var metaFileTuples = await GetAllPakMetadata(pakLoadoutFiles, @@ -67,9 +68,21 @@ private async Task> DiagnosePakModulesAsync(Loadout.Read } // non error case - var metadata = metadataOrError.Result; - var dependencies = metadata.MetaFileData.Dependencies; + var pakMetaData = metadataOrError.Result; + // check if the mod requires the script extender and if SE is missing + if (pakMetaData.ScriptExtenderConfigMetadata is { HasValue: true, Value.RequiresScriptExtender: true } && + !bg3LoadoutFile.HasValue) + { + diagnostics.Add(Diagnostics.CreateMissingRequiredScriptExtender( + ModName: loadoutItemGroup.ToReference(loadout), + PakName:pakMetaData.MetaFileData.ModuleShortDesc.Name, + BG3SENexusLink:BG3SENexusModsLink)); + } + + + // check dependencies + var dependencies = pakMetaData.MetaFileData.Dependencies; foreach (var dependency in dependencies) { var dependencyUuid = dependency.Uuid; @@ -90,8 +103,8 @@ x.Item2.Exception is null && ModName: loadoutItemGroup.ToReference(loadout), MissingDepName: dependency.Name, MissingDepVersion: dependency.SemanticVersion.ToString(), - PakModuleName: metadata.MetaFileData.ModuleShortDesc.Name, - PakModuleVersion: metadata.MetaFileData.ModuleShortDesc.SemanticVersion.ToString(), + PakModuleName: pakMetaData.MetaFileData.ModuleShortDesc.Name, + PakModuleVersion: pakMetaData.MetaFileData.ModuleShortDesc.SemanticVersion.ToString(), NexusModsLink: NexusModsLink ) ); @@ -112,8 +125,8 @@ x.Item2.Exception is null && { diagnostics.Add(Diagnostics.CreateOutdatedDependency( ModName: loadoutItemGroup.ToReference(loadout), - PakModuleName: metadata.MetaFileData.ModuleShortDesc.Name, - PakModuleVersion: metadata.MetaFileData.ModuleShortDesc.SemanticVersion.ToString(), + PakModuleName: pakMetaData.MetaFileData.ModuleShortDesc.Name, + PakModuleVersion: pakMetaData.MetaFileData.ModuleShortDesc.SemanticVersion.ToString(), DepModName: matchLoadoutItemGroup.ToReference(loadout), DepName: installedMatchModule.Name, MinDepVersion: dependency.SemanticVersion.ToString(), @@ -191,6 +204,7 @@ private static LoadoutFile.ReadOnly[] GetAllPakLoadoutFiles( } private static readonly NamedLink NexusModsLink = new("Nexus Mods - Baldur's Gate 3", NexusModsUrlBuilder.CreateGenericUri("https://nexusmods.com/baldursgate3")); + private static readonly NamedLink BG3SENexusModsLink = new("Nexus Mods", NexusModsUrlBuilder.CreateGenericUri("https://www.nexusmods.com/baldursgate3/mods/2172")); #endregion Helpers } From b499ab118a5cba2ee76f39303a80e92da0351796 Mon Sep 17 00:00:00 2001 From: Al12rs <26797547+Al12rs@users.noreply.github.com> Date: Wed, 23 Oct 2024 16:09:19 +0200 Subject: [PATCH 5/5] Add diagnostic for BG3 Script Extender WINEOVERRIDE instructions --- .../BaldursGate3/Diagnostics.cs | 55 +++++++++++++------ .../Emitters/DependencyDiagnosticEmitter.cs | 14 ++++- 2 files changed, 50 insertions(+), 19 deletions(-) diff --git a/src/Games/NexusMods.Games.Larian/BaldursGate3/Diagnostics.cs b/src/Games/NexusMods.Games.Larian/BaldursGate3/Diagnostics.cs index f384867e8b..0d247712a9 100644 --- a/src/Games/NexusMods.Games.Larian/BaldursGate3/Diagnostics.cs +++ b/src/Games/NexusMods.Games.Larian/BaldursGate3/Diagnostics.cs @@ -10,10 +10,8 @@ namespace NexusMods.Games.Larian.BaldursGate3; internal static partial class Diagnostics { private const string Source = "NexusMods.Games.Larian.BaldursGate3"; - - [DiagnosticTemplate] - [UsedImplicitly] - internal static IDiagnosticTemplate MissingDependencyTemplate = DiagnosticTemplateBuilder + + [DiagnosticTemplate] [UsedImplicitly] internal static IDiagnosticTemplate MissingDependencyTemplate = DiagnosticTemplateBuilder .Start() .WithId(new DiagnosticId(Source, number: 1)) .WithTitle("Missing required dependency") @@ -29,7 +27,8 @@ internal static partial class Diagnostics #### Or #### Check the required mods section on {ModName} Nexus Mods page Mod pages can contain useful installation instructions in the 'Description' tab, this tab will also include requirements the mod needs to work correctly. - """) + """ + ) .WithMessageData(messageBuilder => messageBuilder .AddDataReference("ModName") .AddValue("MissingDepName") @@ -39,10 +38,8 @@ internal static partial class Diagnostics .AddValue("NexusModsLink") ) .Finish(); - - [DiagnosticTemplate] - [UsedImplicitly] - internal static IDiagnosticTemplate OutdatedDependencyTemplate = DiagnosticTemplateBuilder + + [DiagnosticTemplate] [UsedImplicitly] internal static IDiagnosticTemplate OutdatedDependencyTemplate = DiagnosticTemplateBuilder .Start() .WithId(new DiagnosticId(Source, number: 2)) .WithTitle("Required dependency is outdated") @@ -50,7 +47,8 @@ internal static partial class Diagnostics .WithSummary("Mod {ModName} requires at least version {MinDepVersion}+ of '{DepName}' but only v{CurrentDepVersion} is installed.") .WithDetails(""" '{PakModuleName}' v{PakModuleVersion} requires at least version {MinDepVersion}+ of '{DepName}' to run correctly. However, you only have version v{CurrentDepVersion} installed in mod {ModName}. - """) + """ + ) .WithMessageData(messageBuilder => messageBuilder .AddDataReference("ModName") .AddValue("PakModuleName") @@ -61,11 +59,9 @@ internal static partial class Diagnostics .AddValue("CurrentDepVersion") ) .Finish(); - - - [DiagnosticTemplate] - [UsedImplicitly] - internal static IDiagnosticTemplate InvalidPakFileTemplate = DiagnosticTemplateBuilder + + + [DiagnosticTemplate] [UsedImplicitly] internal static IDiagnosticTemplate InvalidPakFileTemplate = DiagnosticTemplateBuilder .Start() .WithId(new DiagnosticId(Source, number: 3)) .WithTitle("Invalid pak file") @@ -78,7 +74,8 @@ internal static partial class Diagnostics ## Recommended Actions Verify that the file is installed in the intended location and that it wasn't altered or corrupted. You may need to remove or reinstall the mod, consulting the mod's instructions for proper installation. - """) + """ + ) .WithMessageData(messageBuilder => messageBuilder .AddDataReference("ModName") .AddValue("PakFileName") @@ -101,7 +98,29 @@ Install the BG3 Script Extender from {BG3SENexusLink} or from the official sourc .WithMessageData(messageBuilder => messageBuilder .AddDataReference("ModName") .AddValue("PakName") - .AddValue("BG3SENexusLink")) + .AddValue("BG3SENexusLink") + ) + .Finish(); + + [DiagnosticTemplate] [UsedImplicitly] internal static IDiagnosticTemplate Bg3SeWineDllOverrideSteamTemplate = DiagnosticTemplateBuilder + .Start() + .WithId(new DiagnosticId(Source, number: 5)) + .WithTitle("BG3SE Wine DLL Override required for Steam") + .WithSeverity(DiagnosticSeverity.Suggestion) + .WithSummary("BG3SE Requires WINEDLLOVERRIDE Environment variable to be set") + .WithDetails(""" + In Linux Wine environments, the BG3 Script Extender (BG3SE) requires WINEDLLOVERRIDE environment to contain `"DWrite=n,b"` to work correctly. + Please ensure you have this set correctly in your BG3 Steam properties, under Launch Options: + `WINEDLLOVERRIDES="DWrite=n,b" %command%` + + + ## Details: + BG3SE adds `DWrite.dll` file to the game folder, which replaces a Windows system dll normally located in windows system folders. + On windows the game will automatically load the dll file from the game folder if present, preferring that over the system one. + On Wine, to achieve the same effect, you need to set the WINEDLLOVERRIDE environment variable to tell Wine to load the game's DWrite.dll instead of the system one. + """ + + ) + .WithMessageData(messageBuilder => messageBuilder.AddValue("Template")) .Finish(); - } diff --git a/src/Games/NexusMods.Games.Larian/BaldursGate3/Emitters/DependencyDiagnosticEmitter.cs b/src/Games/NexusMods.Games.Larian/BaldursGate3/Emitters/DependencyDiagnosticEmitter.cs index e9e16f362a..ddcb6e2fee 100644 --- a/src/Games/NexusMods.Games.Larian/BaldursGate3/Emitters/DependencyDiagnosticEmitter.cs +++ b/src/Games/NexusMods.Games.Larian/BaldursGate3/Emitters/DependencyDiagnosticEmitter.cs @@ -5,6 +5,7 @@ using NexusMods.Abstractions.Diagnostics.Emitters; using NexusMods.Abstractions.Diagnostics.References; using NexusMods.Abstractions.Diagnostics.Values; +using NexusMods.Abstractions.Games.Stores.Steam; using NexusMods.Abstractions.Loadouts; using NexusMods.Abstractions.Loadouts.Extensions; using NexusMods.Abstractions.Resources; @@ -12,6 +13,7 @@ using NexusMods.Games.Larian.BaldursGate3.Utils.LsxXmlParsing; using NexusMods.Games.Larian.BaldursGate3.Utils.PakParsing; using NexusMods.Hashing.xxHash64; +using NexusMods.Paths; using Polly; namespace NexusMods.Games.Larian.BaldursGate3.Emitters; @@ -20,16 +22,26 @@ public class DependencyDiagnosticEmitter : ILoadoutDiagnosticEmitter { private readonly ILogger _logger; private readonly IResourceLoader> _metadataPipeline; + private readonly IOSInformation _os; - public DependencyDiagnosticEmitter(IServiceProvider serviceProvider, ILogger logger) + public DependencyDiagnosticEmitter(IServiceProvider serviceProvider, ILogger logger, IOSInformation os) { _logger = logger; _metadataPipeline = Pipelines.GetMetadataPipeline(serviceProvider); + _os = os; } public async IAsyncEnumerable Diagnose(Loadout.ReadOnly loadout, [EnumeratorCancellation] CancellationToken cancellationToken) { var bg3LoadoutFile = GetScriptExtenderLoadoutFile(loadout); + + // BG3SE WINEDLLOVERRIDE diagnostic + if (_os.IsLinux && bg3LoadoutFile.HasValue && loadout.InstallationInstance.LocatorResultMetadata is SteamLocatorResultMetadata) + { + // yield return Diagnostics. + yield return Diagnostics.CreateBg3SeWineDllOverrideSteam(Template: "text"); + } + var diagnostics = await DiagnosePakModulesAsync(loadout, bg3LoadoutFile, cancellationToken); foreach (var diagnostic in diagnostics) {