From a819b75b0c994f2556def801ff30fbf093dbb9f7 Mon Sep 17 00:00:00 2001
From: AL <26797547+Al12rs@users.noreply.github.com>
Date: Mon, 14 Oct 2024 14:01:14 +0200
Subject: [PATCH 01/11] Parse module versions into semantic version struct, to
allow semantic comparisons
---
.../Utils/PakParsing/LsxXmlFormat.cs | 48 +++++++++++++++++++
.../Utils/PakParsing/MetaFileParser.cs | 12 ++++-
...ctly_pakFilePath=AllItems.pak.verified.txt | 3 +-
...ght Increased 9000 - X900.pak.verified.txt | 3 +-
...magicExtendedQuickenedSP2.pak.verified.txt | 3 +-
...th=MoreSpellSlotsAndFeats.pak.verified.txt | 3 +-
...oint Inside Emerald Grove.pak.verified.txt | 11 +++--
...ollegeofswordshomebr-hu6v.pak.verified.txt | 7 ++-
.../BaldursGate3/BG3PakParsingTests.cs | 10 +++-
9 files changed, 88 insertions(+), 12 deletions(-)
diff --git a/src/Games/NexusMods.Games.Larian/BaldursGate3/Utils/PakParsing/LsxXmlFormat.cs b/src/Games/NexusMods.Games.Larian/BaldursGate3/Utils/PakParsing/LsxXmlFormat.cs
index 816158596..07ffa7fff 100644
--- a/src/Games/NexusMods.Games.Larian/BaldursGate3/Utils/PakParsing/LsxXmlFormat.cs
+++ b/src/Games/NexusMods.Games.Larian/BaldursGate3/Utils/PakParsing/LsxXmlFormat.cs
@@ -31,6 +31,54 @@ public struct ModuleShortDesc
public string Version;
public string Uuid;
public string Md5;
+ public ModuleVersion SemanticVersion;
+ }
+
+
+ public struct ModuleVersion
+ {
+ public ulong Major;
+ public ulong Minor;
+ public ulong Patch;
+ public ulong Build;
+
+ public static ModuleVersion FromInt64(UInt64 packed)
+ {
+ return new ModuleVersion
+ {
+ Major = packed >> 55,
+ Minor = (packed >> 47) & 0xFF,
+ Patch = (packed >> 31) & 0xFFFF,
+ Build = packed & 0x7FFFFFFFUL,
+ };
+ }
+
+ public static UInt64 ParseVersion(string? str)
+ {
+ // Even though version is marked as Int32, it could actually contain 64-bit values, so we need to parse it as UInt64
+ if (string.IsNullOrWhiteSpace(str) || !UInt64.TryParse(str, out var result))
+ return 0;
+
+ if (result == 1 || result == 268435456)
+ {
+ // v1.0.0.0
+ return 36028797018963968;
+ }
+ return result;
+ }
+
+ public static UInt64 ParseVersion64(string? str)
+ {
+ if (string.IsNullOrWhiteSpace(str) || !UInt64.TryParse(str, out var result))
+ return 0;
+
+ if (result == 1)
+ {
+ // v1.0.0.0
+ return 36028797018963968;
+ }
+ return result;
+ }
}
diff --git a/src/Games/NexusMods.Games.Larian/BaldursGate3/Utils/PakParsing/MetaFileParser.cs b/src/Games/NexusMods.Games.Larian/BaldursGate3/Utils/PakParsing/MetaFileParser.cs
index e263b068b..76556f657 100644
--- a/src/Games/NexusMods.Games.Larian/BaldursGate3/Utils/PakParsing/MetaFileParser.cs
+++ b/src/Games/NexusMods.Games.Larian/BaldursGate3/Utils/PakParsing/MetaFileParser.cs
@@ -47,9 +47,13 @@ public static LsxXmlFormat.MetaFileData ParseMetaFile(Stream xmlStream)
moduleShortDesc.PublishHandle = value ?? string.Empty;
break;
case "Version64":
+ moduleShortDesc.Version = value ?? string.Empty;
+ moduleShortDesc.SemanticVersion = LsxXmlFormat.ModuleVersion.FromInt64(LsxXmlFormat.ModuleVersion.ParseVersion64(moduleShortDesc.Version));
+ break;
case "Version":
- // TODO: Actually parse the version into something we can compare, which will require different handling depending on Version vs Version64
+ // From older meta files, it seems that it could contain 64-bit values even though type is marked as Int32
moduleShortDesc.Version = value ?? string.Empty;
+ moduleShortDesc.SemanticVersion = LsxXmlFormat.ModuleVersion.FromInt64(LsxXmlFormat.ModuleVersion.ParseVersion(moduleShortDesc.Version));
break;
case "UUID":
moduleShortDesc.Uuid = value ?? string.Empty;
@@ -103,9 +107,13 @@ public static LsxXmlFormat.MetaFileData ParseMetaFile(Stream xmlStream)
dependency.PublishHandle = value ?? string.Empty;
break;
case "Version64":
+ dependency.Version = value ?? string.Empty;
+ dependency.SemanticVersion = LsxXmlFormat.ModuleVersion.FromInt64(LsxXmlFormat.ModuleVersion.ParseVersion64(dependency.Version));
+ break;
case "Version":
- // TODO: Actually parse the version into something we can compare, which will require different handling depending on Version vs Version64
+ // From older meta files, it seems that it could contain 64-bit values even though type is marked as Int32
dependency.Version = value ?? string.Empty;
+ dependency.SemanticVersion = LsxXmlFormat.ModuleVersion.FromInt64(LsxXmlFormat.ModuleVersion.ParseVersion(dependency.Version));
break;
case "UUID":
dependency.Uuid = value ?? string.Empty;
diff --git a/tests/Games/NexusMods.Games.Larian.Tests/BaldursGate3/BG3PakParsingTests.ParsePakMeta_ShouldParseCorrectly_pakFilePath=AllItems.pak.verified.txt b/tests/Games/NexusMods.Games.Larian.Tests/BaldursGate3/BG3PakParsingTests.ParsePakMeta_ShouldParseCorrectly_pakFilePath=AllItems.pak.verified.txt
index 5b3402ced..0228e4819 100644
--- a/tests/Games/NexusMods.Games.Larian.Tests/BaldursGate3/BG3PakParsingTests.ParsePakMeta_ShouldParseCorrectly_pakFilePath=AllItems.pak.verified.txt
+++ b/tests/Games/NexusMods.Games.Larian.Tests/BaldursGate3/BG3PakParsingTests.ParsePakMeta_ShouldParseCorrectly_pakFilePath=AllItems.pak.verified.txt
@@ -6,4 +6,5 @@
-
\ No newline at end of file
+
+Version: 1.0.0.0
diff --git a/tests/Games/NexusMods.Games.Larian.Tests/BaldursGate3/BG3PakParsingTests.ParsePakMeta_ShouldParseCorrectly_pakFilePath=Carry Weight Increased 9000 - X900.pak.verified.txt b/tests/Games/NexusMods.Games.Larian.Tests/BaldursGate3/BG3PakParsingTests.ParsePakMeta_ShouldParseCorrectly_pakFilePath=Carry Weight Increased 9000 - X900.pak.verified.txt
index 2cda33148..34be18b26 100644
--- a/tests/Games/NexusMods.Games.Larian.Tests/BaldursGate3/BG3PakParsingTests.ParsePakMeta_ShouldParseCorrectly_pakFilePath=Carry Weight Increased 9000 - X900.pak.verified.txt
+++ b/tests/Games/NexusMods.Games.Larian.Tests/BaldursGate3/BG3PakParsingTests.ParsePakMeta_ShouldParseCorrectly_pakFilePath=Carry Weight Increased 9000 - X900.pak.verified.txt
@@ -6,4 +6,5 @@
-
\ No newline at end of file
+
+Version: 1.0.0.0
diff --git a/tests/Games/NexusMods.Games.Larian.Tests/BaldursGate3/BG3PakParsingTests.ParsePakMeta_ShouldParseCorrectly_pakFilePath=MetamagicExtendedQuickenedSP2.pak.verified.txt b/tests/Games/NexusMods.Games.Larian.Tests/BaldursGate3/BG3PakParsingTests.ParsePakMeta_ShouldParseCorrectly_pakFilePath=MetamagicExtendedQuickenedSP2.pak.verified.txt
index 03e63d9e3..268ea5b76 100644
--- a/tests/Games/NexusMods.Games.Larian.Tests/BaldursGate3/BG3PakParsingTests.ParsePakMeta_ShouldParseCorrectly_pakFilePath=MetamagicExtendedQuickenedSP2.pak.verified.txt
+++ b/tests/Games/NexusMods.Games.Larian.Tests/BaldursGate3/BG3PakParsingTests.ParsePakMeta_ShouldParseCorrectly_pakFilePath=MetamagicExtendedQuickenedSP2.pak.verified.txt
@@ -6,4 +6,5 @@
-
\ No newline at end of file
+
+Version: 4.0.0.0
diff --git a/tests/Games/NexusMods.Games.Larian.Tests/BaldursGate3/BG3PakParsingTests.ParsePakMeta_ShouldParseCorrectly_pakFilePath=MoreSpellSlotsAndFeats.pak.verified.txt b/tests/Games/NexusMods.Games.Larian.Tests/BaldursGate3/BG3PakParsingTests.ParsePakMeta_ShouldParseCorrectly_pakFilePath=MoreSpellSlotsAndFeats.pak.verified.txt
index f434c38f2..f13aecb28 100644
--- a/tests/Games/NexusMods.Games.Larian.Tests/BaldursGate3/BG3PakParsingTests.ParsePakMeta_ShouldParseCorrectly_pakFilePath=MoreSpellSlotsAndFeats.pak.verified.txt
+++ b/tests/Games/NexusMods.Games.Larian.Tests/BaldursGate3/BG3PakParsingTests.ParsePakMeta_ShouldParseCorrectly_pakFilePath=MoreSpellSlotsAndFeats.pak.verified.txt
@@ -6,4 +6,5 @@
-
\ No newline at end of file
+
+Version: 1.0.1.1
diff --git a/tests/Games/NexusMods.Games.Larian.Tests/BaldursGate3/BG3PakParsingTests.ParsePakMeta_ShouldParseCorrectly_pakFilePath=Waypoint Inside Emerald Grove.pak.verified.txt b/tests/Games/NexusMods.Games.Larian.Tests/BaldursGate3/BG3PakParsingTests.ParsePakMeta_ShouldParseCorrectly_pakFilePath=Waypoint Inside Emerald Grove.pak.verified.txt
index 97329b092..5fa692dd3 100644
--- a/tests/Games/NexusMods.Games.Larian.Tests/BaldursGate3/BG3PakParsingTests.ParsePakMeta_ShouldParseCorrectly_pakFilePath=Waypoint Inside Emerald Grove.pak.verified.txt
+++ b/tests/Games/NexusMods.Games.Larian.Tests/BaldursGate3/BG3PakParsingTests.ParsePakMeta_ShouldParseCorrectly_pakFilePath=Waypoint Inside Emerald Grove.pak.verified.txt
@@ -6,7 +6,9 @@
-Dependency:
+
+Version: 2.0.2.0
+Dependency:
@@ -14,7 +16,9 @@
-Dependency:
+
+SemanticVersion: 1.0.1.0
+Dependency:
@@ -22,4 +26,5 @@
-
\ No newline at end of file
+
+SemanticVersion: 1.0.0.0
diff --git a/tests/Games/NexusMods.Games.Larian.Tests/BaldursGate3/BG3PakParsingTests.ParsePakMeta_ShouldParseCorrectly_pakFilePath=dnd5rbardcollegeofswordshomebr-hu6v.pak.verified.txt b/tests/Games/NexusMods.Games.Larian.Tests/BaldursGate3/BG3PakParsingTests.ParsePakMeta_ShouldParseCorrectly_pakFilePath=dnd5rbardcollegeofswordshomebr-hu6v.pak.verified.txt
index 77157c7ac..6e2ba755b 100644
--- a/tests/Games/NexusMods.Games.Larian.Tests/BaldursGate3/BG3PakParsingTests.ParsePakMeta_ShouldParseCorrectly_pakFilePath=dnd5rbardcollegeofswordshomebr-hu6v.pak.verified.txt
+++ b/tests/Games/NexusMods.Games.Larian.Tests/BaldursGate3/BG3PakParsingTests.ParsePakMeta_ShouldParseCorrectly_pakFilePath=dnd5rbardcollegeofswordshomebr-hu6v.pak.verified.txt
@@ -6,7 +6,9 @@
-Dependency:
+
+Version: 1.0.0.1
+Dependency:
@@ -14,4 +16,5 @@
-
\ No newline at end of file
+
+SemanticVersion: 1.1.1.0
diff --git a/tests/Games/NexusMods.Games.Larian.Tests/BaldursGate3/BG3PakParsingTests.cs b/tests/Games/NexusMods.Games.Larian.Tests/BaldursGate3/BG3PakParsingTests.cs
index 8f816a155..36e6ea66b 100644
--- a/tests/Games/NexusMods.Games.Larian.Tests/BaldursGate3/BG3PakParsingTests.cs
+++ b/tests/Games/NexusMods.Games.Larian.Tests/BaldursGate3/BG3PakParsingTests.cs
@@ -27,12 +27,19 @@ public async Task ParsePakMeta_ShouldParseCorrectly(string pakFilePath)
await using var pakFileStream = File.OpenRead(fullPath.ToString());
var metaFileData = PakFileParser.ParsePakMeta(pakFileStream);
var sb = new StringBuilder();
+
sb.AppendLine("ModuleShortDesc:");
sb.Append(LsxXmlFormat.SerializeModuleShortDesc(metaFileData.ModuleShortDesc));
+ sb.AppendLineN();
+ var semanticVersion = metaFileData.ModuleShortDesc.SemanticVersion;
+ sb.AppendLine($"Version: {semanticVersion.Major}.{semanticVersion.Minor}.{semanticVersion.Patch}.{semanticVersion.Build}");
+
foreach (var dependency in metaFileData.Dependencies)
{
sb.AppendLine("Dependency:");
sb.Append(LsxXmlFormat.SerializeModuleShortDesc(dependency));
+ sb.AppendLineN();
+ sb.AppendLine($"SemanticVersion: {dependency.SemanticVersion.Major}.{dependency.SemanticVersion.Minor}.{dependency.SemanticVersion.Patch}.{dependency.SemanticVersion.Build}");
}
await Verify(sb.ToString()).UseParameters(pakFilePath);
}
@@ -46,6 +53,7 @@ public async Task ParsePakMeta_ShouldThrowOnBadPak()
var act = () => PakFileParser.ParsePakMeta(pakFileStream);
act.Should().Throw();
}
-
}
+
+
}
From b1da1fbb480fe19ef855a571609911f8cda1f80f Mon Sep 17 00:00:00 2001
From: AL <26797547+Al12rs@users.noreply.github.com>
Date: Mon, 14 Oct 2024 14:51:58 +0200
Subject: [PATCH 02/11] Wip reorganize BG3 utils files
---
.../BaldursGate3/Diagnostics.cs | 16 ++++++++++++----
.../Emitters/DependencyDiagnosticEmitter.cs | 3 ++-
.../BaldursGate3/Pipelines.cs | 1 +
.../LsxXmlFormat.cs | 3 +--
.../MetaFileParser.cs | 2 +-
.../ModsettingsFileFormat.cs | 3 +--
.../Utils/PakParsing/PakFileParser.cs | 1 +
.../BG3ModsettingsFileFormatTests.cs | 1 +
.../BaldursGate3/BG3PakParsingTests.cs | 1 +
9 files changed, 21 insertions(+), 10 deletions(-)
rename src/Games/NexusMods.Games.Larian/BaldursGate3/Utils/{PakParsing => LsxXmlParsing}/LsxXmlFormat.cs (98%)
rename src/Games/NexusMods.Games.Larian/BaldursGate3/Utils/{PakParsing => LsxXmlParsing}/MetaFileParser.cs (99%)
rename src/Games/NexusMods.Games.Larian/BaldursGate3/Utils/{PakParsing => LsxXmlParsing}/ModsettingsFileFormat.cs (98%)
diff --git a/src/Games/NexusMods.Games.Larian/BaldursGate3/Diagnostics.cs b/src/Games/NexusMods.Games.Larian/BaldursGate3/Diagnostics.cs
index ba673d982..a15fb2a8a 100644
--- a/src/Games/NexusMods.Games.Larian/BaldursGate3/Diagnostics.cs
+++ b/src/Games/NexusMods.Games.Larian/BaldursGate3/Diagnostics.cs
@@ -18,16 +18,24 @@ internal static partial class Diagnostics
.WithId(new DiagnosticId(Source, number: 1))
.WithTitle("Missing required dependency")
.WithSeverity(DiagnosticSeverity.Warning)
- .WithSummary("Mod {PakMod} is missing required dependency '{MissingDependencyName}'.")
+ .WithSummary("The mod `{ModName}` is missing the required dependency '{MissingDepName}' v{MissingDepVersion}.")
.WithDetails("""
- '{MissingDependencyName}' is required by '{PakModuleName}' but is not present in the loadout.
+
+ '{MissingDependencyName}' v{MissingDevVersion} is not installed or enabled in your Loadout. This pak module is required by `{PakModuleName}` v{PakModuleVersion} to run correct.
+
+ ## Recommended actions
+ ### Search for and install the missing mod
+ You can search for '{MissingDependencyName}' on {NexusModsLink}
+
You can try to search the missing mod on {NexusModsLink} or using the in-game mod manager.
""")
.WithMessageData(messageBuilder => messageBuilder
- .AddDataReference("PakMod")
- .AddValue("MissingDependencyName")
+ .AddDataReference("ModName")
+ .AddValue("MissingDepName")
.AddValue("PakModuleName")
+ .AddValue("PakModuleVersion")
+ .AddValue("MissingDepVersion")
.AddValue("NexusModsLink")
)
.Finish();
diff --git a/src/Games/NexusMods.Games.Larian/BaldursGate3/Emitters/DependencyDiagnosticEmitter.cs b/src/Games/NexusMods.Games.Larian/BaldursGate3/Emitters/DependencyDiagnosticEmitter.cs
index 258372630..cca5f2fc1 100644
--- a/src/Games/NexusMods.Games.Larian/BaldursGate3/Emitters/DependencyDiagnosticEmitter.cs
+++ b/src/Games/NexusMods.Games.Larian/BaldursGate3/Emitters/DependencyDiagnosticEmitter.cs
@@ -7,6 +7,7 @@
using NexusMods.Abstractions.Loadouts;
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;
@@ -62,7 +63,7 @@ private async Task> DiagnoseDependenciesAsync(Loadout.Re
// add diagnostic
diagnostics.Add(Diagnostics.CreateMissingDependency(
- PakMod: loadoutItemGroup.ToReference(loadout),
+ ModuleName: loadoutItemGroup.ToReference(loadout),
MissingDependencyName: dependency.Name,
PakModuleName: metaFileData.Item2.ModuleShortDesc.Name,
NexusModsLink: NexusModsLink
diff --git a/src/Games/NexusMods.Games.Larian/BaldursGate3/Pipelines.cs b/src/Games/NexusMods.Games.Larian/BaldursGate3/Pipelines.cs
index ba6ea20b0..1adc79f7a 100644
--- a/src/Games/NexusMods.Games.Larian/BaldursGate3/Pipelines.cs
+++ b/src/Games/NexusMods.Games.Larian/BaldursGate3/Pipelines.cs
@@ -6,6 +6,7 @@
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;
diff --git a/src/Games/NexusMods.Games.Larian/BaldursGate3/Utils/PakParsing/LsxXmlFormat.cs b/src/Games/NexusMods.Games.Larian/BaldursGate3/Utils/LsxXmlParsing/LsxXmlFormat.cs
similarity index 98%
rename from src/Games/NexusMods.Games.Larian/BaldursGate3/Utils/PakParsing/LsxXmlFormat.cs
rename to src/Games/NexusMods.Games.Larian/BaldursGate3/Utils/LsxXmlParsing/LsxXmlFormat.cs
index 07ffa7fff..156c72bc8 100644
--- a/src/Games/NexusMods.Games.Larian/BaldursGate3/Utils/PakParsing/LsxXmlFormat.cs
+++ b/src/Games/NexusMods.Games.Larian/BaldursGate3/Utils/LsxXmlParsing/LsxXmlFormat.cs
@@ -1,7 +1,6 @@
-using System.Text;
using System.Xml;
-namespace NexusMods.Games.Larian.BaldursGate3.Utils.PakParsing;
+namespace NexusMods.Games.Larian.BaldursGate3.Utils.LsxXmlParsing;
///
/// Class containing definitions for the Larian Xml (LSX) format.
diff --git a/src/Games/NexusMods.Games.Larian/BaldursGate3/Utils/PakParsing/MetaFileParser.cs b/src/Games/NexusMods.Games.Larian/BaldursGate3/Utils/LsxXmlParsing/MetaFileParser.cs
similarity index 99%
rename from src/Games/NexusMods.Games.Larian/BaldursGate3/Utils/PakParsing/MetaFileParser.cs
rename to src/Games/NexusMods.Games.Larian/BaldursGate3/Utils/LsxXmlParsing/MetaFileParser.cs
index 76556f657..2de591350 100644
--- a/src/Games/NexusMods.Games.Larian/BaldursGate3/Utils/PakParsing/MetaFileParser.cs
+++ b/src/Games/NexusMods.Games.Larian/BaldursGate3/Utils/LsxXmlParsing/MetaFileParser.cs
@@ -1,6 +1,6 @@
using System.Xml;
-namespace NexusMods.Games.Larian.BaldursGate3.Utils.PakParsing
+namespace NexusMods.Games.Larian.BaldursGate3.Utils.LsxXmlParsing
{
///
/// Class to parse and extract dependencies and metadata from a bg3 `meta.lsx` file.
diff --git a/src/Games/NexusMods.Games.Larian/BaldursGate3/Utils/PakParsing/ModsettingsFileFormat.cs b/src/Games/NexusMods.Games.Larian/BaldursGate3/Utils/LsxXmlParsing/ModsettingsFileFormat.cs
similarity index 98%
rename from src/Games/NexusMods.Games.Larian/BaldursGate3/Utils/PakParsing/ModsettingsFileFormat.cs
rename to src/Games/NexusMods.Games.Larian/BaldursGate3/Utils/LsxXmlParsing/ModsettingsFileFormat.cs
index ba599439a..7673a69ef 100644
--- a/src/Games/NexusMods.Games.Larian/BaldursGate3/Utils/PakParsing/ModsettingsFileFormat.cs
+++ b/src/Games/NexusMods.Games.Larian/BaldursGate3/Utils/LsxXmlParsing/ModsettingsFileFormat.cs
@@ -1,7 +1,6 @@
-using System.Text;
using System.Xml;
-namespace NexusMods.Games.Larian.BaldursGate3.Utils.PakParsing;
+namespace NexusMods.Games.Larian.BaldursGate3.Utils.LsxXmlParsing;
///
/// Class to parse and write the `modsettings.lsx` xml file format used for the BG3 Load Order.
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 f684bfb5c..67c37e486 100644
--- a/src/Games/NexusMods.Games.Larian/BaldursGate3/Utils/PakParsing/PakFileParser.cs
+++ b/src/Games/NexusMods.Games.Larian/BaldursGate3/Utils/PakParsing/PakFileParser.cs
@@ -2,6 +2,7 @@
using System.Text;
using DynamicData.Kernel;
using K4os.Compression.LZ4;
+using NexusMods.Games.Larian.BaldursGate3.Utils.LsxXmlParsing;
using ZstdSharp;
namespace NexusMods.Games.Larian.BaldursGate3.Utils.PakParsing;
diff --git a/tests/Games/NexusMods.Games.Larian.Tests/BaldursGate3/BG3ModsettingsFileFormatTests.cs b/tests/Games/NexusMods.Games.Larian.Tests/BaldursGate3/BG3ModsettingsFileFormatTests.cs
index fa1a56b71..469a5dd6d 100644
--- a/tests/Games/NexusMods.Games.Larian.Tests/BaldursGate3/BG3ModsettingsFileFormatTests.cs
+++ b/tests/Games/NexusMods.Games.Larian.Tests/BaldursGate3/BG3ModsettingsFileFormatTests.cs
@@ -1,4 +1,5 @@
using System.Text;
+using NexusMods.Games.Larian.BaldursGate3.Utils.LsxXmlParsing;
using NexusMods.Games.Larian.BaldursGate3.Utils.PakParsing;
using NexusMods.Paths;
diff --git a/tests/Games/NexusMods.Games.Larian.Tests/BaldursGate3/BG3PakParsingTests.cs b/tests/Games/NexusMods.Games.Larian.Tests/BaldursGate3/BG3PakParsingTests.cs
index 36e6ea66b..a94f60aac 100644
--- a/tests/Games/NexusMods.Games.Larian.Tests/BaldursGate3/BG3PakParsingTests.cs
+++ b/tests/Games/NexusMods.Games.Larian.Tests/BaldursGate3/BG3PakParsingTests.cs
@@ -1,5 +1,6 @@
using System.Text;
using FluentAssertions;
+using NexusMods.Games.Larian.BaldursGate3.Utils.LsxXmlParsing;
using NexusMods.Games.Larian.BaldursGate3.Utils.PakParsing;
using NexusMods.Paths;
From 6dc15713d484a4b6d9ac373e72df7c6e70673117 Mon Sep 17 00:00:00 2001
From: AL <26797547+Al12rs@users.noreply.github.com>
Date: Mon, 14 Oct 2024 16:30:44 +0200
Subject: [PATCH 03/11] Add versions info to helth check message
---
.../BaldursGate3/Diagnostics.cs | 16 ++++++++--------
.../Emitters/DependencyDiagnosticEmitter.cs | 8 +++++---
.../Utils/LsxXmlParsing/LsxXmlFormat.cs | 2 ++
3 files changed, 15 insertions(+), 11 deletions(-)
diff --git a/src/Games/NexusMods.Games.Larian/BaldursGate3/Diagnostics.cs b/src/Games/NexusMods.Games.Larian/BaldursGate3/Diagnostics.cs
index a15fb2a8a..a133309d9 100644
--- a/src/Games/NexusMods.Games.Larian/BaldursGate3/Diagnostics.cs
+++ b/src/Games/NexusMods.Games.Larian/BaldursGate3/Diagnostics.cs
@@ -18,24 +18,24 @@ internal static partial class Diagnostics
.WithId(new DiagnosticId(Source, number: 1))
.WithTitle("Missing required dependency")
.WithSeverity(DiagnosticSeverity.Warning)
- .WithSummary("The mod `{ModName}` is missing the required dependency '{MissingDepName}' v{MissingDepVersion}.")
+ .WithSummary("The mod {ModName} is missing the required dependency '{MissingDepName}' v{MissingDepVersion}.")
.WithDetails("""
+ '{MissingDepName}' v{MissingDepVersion} is not installed or enabled in your Loadout. This pak module is required by '{PakModuleName}' v{PakModuleVersion} to run correctly.
- '{MissingDependencyName}' v{MissingDevVersion} is not installed or enabled in your Loadout. This pak module is required by `{PakModuleName}` v{PakModuleVersion} to run correct.
## Recommended actions
- ### Search for and install the missing mod
- You can search for '{MissingDependencyName}' on {NexusModsLink}
-
-
- You can try to search the missing mod on {NexusModsLink} or using the in-game mod manager.
+ #### Search for and install the missing mod
+ You can search for '{MissingDepName}' on {NexusModsLink} or search the in-game mod manager.
+ #### Or
+ #### Check the required mods section on {ModName} NexusMods 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")
+ .AddValue("MissingDepVersion")
.AddValue("PakModuleName")
.AddValue("PakModuleVersion")
- .AddValue("MissingDepVersion")
.AddValue("NexusModsLink")
)
.Finish();
diff --git a/src/Games/NexusMods.Games.Larian/BaldursGate3/Emitters/DependencyDiagnosticEmitter.cs b/src/Games/NexusMods.Games.Larian/BaldursGate3/Emitters/DependencyDiagnosticEmitter.cs
index cca5f2fc1..db67f0b9a 100644
--- a/src/Games/NexusMods.Games.Larian/BaldursGate3/Emitters/DependencyDiagnosticEmitter.cs
+++ b/src/Games/NexusMods.Games.Larian/BaldursGate3/Emitters/DependencyDiagnosticEmitter.cs
@@ -63,9 +63,11 @@ private async Task> DiagnoseDependenciesAsync(Loadout.Re
// add diagnostic
diagnostics.Add(Diagnostics.CreateMissingDependency(
- ModuleName: loadoutItemGroup.ToReference(loadout),
- MissingDependencyName: dependency.Name,
+ ModName: loadoutItemGroup.ToReference(loadout),
+ MissingDepName: dependency.Name,
+ MissingDepVersion: dependency.SemanticVersion.ToString(),
PakModuleName: metaFileData.Item2.ModuleShortDesc.Name,
+ PakModuleVersion: metaFileData.Item2.ModuleShortDesc.SemanticVersion.ToString(),
NexusModsLink: NexusModsLink
)
);
@@ -119,7 +121,7 @@ private static LoadoutFile.ReadOnly[] GetAllPakLoadoutFiles(
.ToArray();
}
- private static readonly NamedLink NexusModsLink = new("Nexus Mods", NexusModsUrlBuilder.CreateGenericUri("https://nexusmods.com/baldursgate3"));
+ private static readonly NamedLink NexusModsLink = new("Nexus Mods - Baldur's Gate 3", NexusModsUrlBuilder.CreateGenericUri("https://nexusmods.com/baldursgate3"));
#endregion Helpers
}
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 156c72bc8..f37388c12 100644
--- a/src/Games/NexusMods.Games.Larian/BaldursGate3/Utils/LsxXmlParsing/LsxXmlFormat.cs
+++ b/src/Games/NexusMods.Games.Larian/BaldursGate3/Utils/LsxXmlParsing/LsxXmlFormat.cs
@@ -78,6 +78,8 @@ public static UInt64 ParseVersion64(string? str)
}
return result;
}
+
+ public override string ToString() => $"{Major}.{Minor}.{Patch}";
}
From d4744d9ed9000679b83b19ab353fb71902a1f2f6 Mon Sep 17 00:00:00 2001
From: AL <26797547+Al12rs@users.noreply.github.com>
Date: Tue, 15 Oct 2024 12:07:06 +0200
Subject: [PATCH 04/11] Streamline pipeline behavior in case of errors during
Pak parsing: - Cache the error result to avoid parsing the file each
subsequent time. - Emit a diagnostic message in case an invalid pak file is
present.
---
.../BaldursGate3/Diagnostics.cs | 23 +++++++++
.../Emitters/DependencyDiagnosticEmitter.cs | 51 +++++++++++++------
.../BaldursGate3/Pipelines.cs | 31 +++++++----
.../Utils/PakParsing/PakFileParser.cs | 18 +++----
4 files changed, 88 insertions(+), 35 deletions(-)
diff --git a/src/Games/NexusMods.Games.Larian/BaldursGate3/Diagnostics.cs b/src/Games/NexusMods.Games.Larian/BaldursGate3/Diagnostics.cs
index a133309d9..b3413bd18 100644
--- a/src/Games/NexusMods.Games.Larian/BaldursGate3/Diagnostics.cs
+++ b/src/Games/NexusMods.Games.Larian/BaldursGate3/Diagnostics.cs
@@ -40,4 +40,27 @@ internal static partial class Diagnostics
)
.Finish();
+
+ [DiagnosticTemplate]
+ [UsedImplicitly]
+ internal static IDiagnosticTemplate InvalidPakFileTemplate = DiagnosticTemplateBuilder
+ .Start()
+ .WithId(new DiagnosticId(Source, number: 1))
+ .WithTitle("Invalid pak file")
+ .WithSeverity(DiagnosticSeverity.Warning)
+ .WithSummary("The file '{PakFileName}' in mod {ModName} was not recognized as a valid Pak file.")
+ .WithDetails("""
+ The app was unable to recognize the file '{PakFileName}' in mod {ModName} as a valid Pak file.
+ This file is unlikely to work correctly in the game.
+
+
+ ## Recommended actions
+ Reinstall the mod or remove the file.
+ """)
+ .WithMessageData(messageBuilder => messageBuilder
+ .AddDataReference("ModName")
+ .AddValue("PakFileName")
+ )
+ .Finish();
+
}
diff --git a/src/Games/NexusMods.Games.Larian/BaldursGate3/Emitters/DependencyDiagnosticEmitter.cs b/src/Games/NexusMods.Games.Larian/BaldursGate3/Emitters/DependencyDiagnosticEmitter.cs
index db67f0b9a..dba6fa9dd 100644
--- a/src/Games/NexusMods.Games.Larian/BaldursGate3/Emitters/DependencyDiagnosticEmitter.cs
+++ b/src/Games/NexusMods.Games.Larian/BaldursGate3/Emitters/DependencyDiagnosticEmitter.cs
@@ -8,15 +8,15 @@
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 OneOf.Types;
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)
{
@@ -26,7 +26,7 @@ public DependencyDiagnosticEmitter(IServiceProvider serviceProvider, ILogger Diagnose(Loadout.ReadOnly loadout, [EnumeratorCancellation] CancellationToken cancellationToken)
{
- var diagnostics = await DiagnoseDependenciesAsync(loadout, cancellationToken);
+ var diagnostics = await DiagnosePakModulesAsync(loadout, cancellationToken);
foreach (var diagnostic in diagnostics)
{
yield return diagnostic;
@@ -35,10 +35,10 @@ public async IAsyncEnumerable Diagnose(Loadout.ReadOnly loadout, [En
#region Diagnosers
- private async Task> DiagnoseDependenciesAsync(Loadout.ReadOnly loadout, CancellationToken cancellationToken)
+ private async Task> DiagnosePakModulesAsync(Loadout.ReadOnly loadout, CancellationToken cancellationToken)
{
var pakLoadoutFiles = GetAllPakLoadoutFiles(loadout, onlyEnabledMods: true);
- var allFileMetadata = await GetAllPakMetadata(pakLoadoutFiles,
+ var metaFileTuples = await GetAllPakMetadata(pakLoadoutFiles,
_metadataPipeline,
_logger,
cancellationToken
@@ -47,16 +47,31 @@ private async Task> DiagnoseDependenciesAsync(Loadout.Re
var diagnostics = new List();
- foreach (var metaFileData in allFileMetadata)
+ foreach (var metaFileTuple in metaFileTuples)
{
- var dependencies = metaFileData.Item2.Dependencies;
- var loadoutItemGroup = metaFileData.Item1.AsLoadoutItemWithTargetPath().AsLoadoutItem().Parent;
+ var (mod, metadataOrError) = metaFileTuple;
+ var loadoutItemGroup = mod.AsLoadoutItemWithTargetPath().AsLoadoutItem().Parent;
+
+ // error case
+ if (metadataOrError.IsT1)
+ {
+ diagnostics.Add(Diagnostics.CreateInvalidPakFile(
+ ModName: loadoutItemGroup.ToReference(loadout),
+ PakFileName: mod.AsLoadoutItemWithTargetPath().TargetPath.Item3.FileName
+ ));
+ continue;
+ }
+
+ // non error case
+ var metadata = metadataOrError.AsT0;
+ var dependencies = metadata.Dependencies;
+
foreach (var dependency in dependencies)
{
var dependencyUuid = dependency.Uuid;
- if (dependencyUuid == string.Empty || allFileMetadata.Any(x => x.Item2.ModuleShortDesc.Uuid == dependencyUuid))
+ if (dependencyUuid == string.Empty || metaFileTuples.Any(x => x.Item2.IsT0 && x.Item2.AsT0.ModuleShortDesc.Uuid == dependencyUuid))
{
continue;
}
@@ -66,8 +81,8 @@ private async Task> DiagnoseDependenciesAsync(Loadout.Re
ModName: loadoutItemGroup.ToReference(loadout),
MissingDepName: dependency.Name,
MissingDepVersion: dependency.SemanticVersion.ToString(),
- PakModuleName: metaFileData.Item2.ModuleShortDesc.Name,
- PakModuleVersion: metaFileData.Item2.ModuleShortDesc.SemanticVersion.ToString(),
+ PakModuleName: metadata.ModuleShortDesc.Name,
+ PakModuleVersion: metadata.ModuleShortDesc.SemanticVersion.ToString(),
NexusModsLink: NexusModsLink
)
);
@@ -81,15 +96,15 @@ private async Task> DiagnoseDependenciesAsync(Loadout.Re
#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);
@@ -99,7 +114,13 @@ private async Task> DiagnoseDependenciesAsync(Loadout.Re
logger.LogError(e, "Exception while getting metadata for `{Name}`", pakLoadoutFile.AsLoadoutItemWithTargetPath().TargetPath.Item3);
continue;
}
-
+
+ // Log the InvalidDataException case, but still return the resource
+ if (resource.Data.IsT1)
+ {
+ logger.LogWarning(resource.Data.AsT1.Value, "Detected invalid BG3 Pak file: `{Name}`", pakLoadoutFile.AsLoadoutItemWithTargetPath().TargetPath.Item3);
+ }
+
yield return (pakLoadoutFile, resource.Data);
}
}
diff --git a/src/Games/NexusMods.Games.Larian/BaldursGate3/Pipelines.cs b/src/Games/NexusMods.Games.Larian/BaldursGate3/Pipelines.cs
index 1adc79f7a..8e759be90 100644
--- a/src/Games/NexusMods.Games.Larian/BaldursGate3/Pipelines.cs
+++ b/src/Games/NexusMods.Games.Larian/BaldursGate3/Pipelines.cs
@@ -1,5 +1,4 @@
using System.Reactive;
-using System.Text;
using BitFaster.Caching.Lru;
using Microsoft.Extensions.DependencyInjection;
using NexusMods.Abstractions.IO;
@@ -9,36 +8,46 @@
using NexusMods.Games.Larian.BaldursGate3.Utils.LsxXmlParsing;
using NexusMods.Games.Larian.BaldursGate3.Utils.PakParsing;
using NexusMods.Hashing.xxHash64;
+using OneOf.Types;
namespace NexusMods.Games.Larian.BaldursGate3;
public static class Pipelines
{
public const string MetadataPipelineKey = nameof(MetadataPipelineKey);
-
+
public static IServiceCollection AddPipelines(this IServiceCollection serviceCollection)
{
- return serviceCollection.AddKeyedSingleton>(
+ return serviceCollection.AddKeyedSingleton>>>(
serviceKey: MetadataPipelineKey,
implementationFactory: static (serviceProvider, _) => CreateMetadataPipeline(
fileStore: serviceProvider.GetRequiredService()
)
);
}
-
- public static IResourceLoader GetMetadataPipeline(IServiceProvider serviceProvider)
+
+ public static IResourceLoader>> GetMetadataPipeline(IServiceProvider serviceProvider)
{
- return serviceProvider.GetRequiredKeyedService>(serviceKey: MetadataPipelineKey);
+ return serviceProvider.GetRequiredKeyedService>>>(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)
- .ThenDo(Unit.Default,
+ .ThenDo>, Stream, Unit>(Unit.Default,
static (_, _, resource, _) =>
{
- var metaFileData = PakFileParser.ParsePakMeta(resource.Data);
- return ValueTask.FromResult(resource.WithData(metaFileData));
+ try
+ {
+ var metaFileData = PakFileParser.ParsePakMeta(resource.Data);
+ return ValueTask.FromResult(resource.WithData(OneOf.OneOf>.FromT0(metaFileData)));
+ }
+ catch (InvalidDataException e)
+ {
+ return ValueTask.FromResult(resource.WithData(OneOf.OneOf>.FromT1(new Error(e))));
+ }
}
)
.UseCache(
@@ -46,7 +55,7 @@ public static IServiceCollection AddPipelines(this IServiceCollection serviceCol
keyComparer: EqualityComparer.Default,
capacityPartition: new FavorWarmPartition(totalCapacity: 300)
);
-
+
return pipeline;
}
}
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 67c37e486..70061ecbe 100644
--- a/src/Games/NexusMods.Games.Larian/BaldursGate3/Utils/PakParsing/PakFileParser.cs
+++ b/src/Games/NexusMods.Games.Larian/BaldursGate3/Utils/PakParsing/PakFileParser.cs
@@ -29,7 +29,7 @@ public static LsxXmlFormat.MetaFileData ParsePakMeta(Stream pakFileStream)
var fileEntryInfo = fileList.FirstOrOptional(f => f.Name.Contains("meta.lsx"));
if (!fileEntryInfo.HasValue)
{
- throw new InvalidDataException($"File meta.lsx not found in pak archive.");
+ throw new InvalidDataException($"Unable to find `meta.lsx` file in pak archive");
}
var metaStream = ReadFileEntryData(br, fileEntryInfo.Value);
@@ -54,7 +54,7 @@ private static LspkPackageFormat.HeaderCommon ParseHeaderInternal(BinaryReader b
if (signature != LspkPackageFormat.HeaderCommon.SIGNATURE_STRING)
{
- throw new InvalidDataException($"Not a valid BG3 PAK. Magic signature {signature} does not match ({LspkPackageFormat.HeaderCommon.SIGNATURE_STRING}).");
+ throw new InvalidDataException($"Not a valid BG3 PAK. Magic signature `{signature}` does not match expected signature `{LspkPackageFormat.HeaderCommon.SIGNATURE_STRING}`");
}
var version = br.ReadUInt32();
@@ -85,7 +85,7 @@ private static LspkPackageFormat.HeaderCommon ParseHeaderInternal(BinaryReader b
NumParts = br.ReadUInt16(),
}.ToCommonHeader();
default:
- throw new InvalidDataException($"Pak version v{version} not supported.");
+ throw new InvalidDataException($"Unrecognized Pak version: v{version}");
}
}
@@ -113,7 +113,7 @@ private static LspkPackageFormat.HeaderCommon ParseHeaderInternal(BinaryReader b
if (numDecodedBytes != decompressedBytes.Length)
{
- throw new InvalidDataException($"Decompression failed: decompressed size {decompressedBytes.Length} does not match expected size {numDecodedBytes}.");
+ throw new InvalidDataException($"Decompression failed: decompressed size {decompressedBytes.Length} does not match expected size {numDecodedBytes}");
}
// new mem stream from decompress bytes
@@ -168,7 +168,7 @@ private static LspkPackageFormat.FileEntryInfoCommon ParseFileEntryInternal(Bina
}.ToCommonFileEntry();
}
default:
- throw new InvalidDataException($"Pak version v{version} not supported.");
+ throw new InvalidDataException($"Unrecognized Pak version: v{version}");
}
}
@@ -184,7 +184,7 @@ private static Stream ReadFileEntryData(BinaryReader br, LspkPackageFormat.FileE
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}.")
+ _ => throw new InvalidDataException($"Unsupported compression method {fileMeta.Flags.Method()} for file {fileMeta.Name}")
};
Stream DecompressLz4(LspkPackageFormat.FileEntryInfoCommon fileEntryInfoCommon, byte[] bytes)
@@ -194,7 +194,7 @@ Stream DecompressLz4(LspkPackageFormat.FileEntryInfoCommon fileEntryInfoCommon,
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);
@@ -208,7 +208,7 @@ 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);
@@ -222,7 +222,7 @@ 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);
From 9915ed21b6754173d355962e85f67c4176f26387 Mon Sep 17 00:00:00 2001
From: AL <26797547+Al12rs@users.noreply.github.com>
Date: Tue, 15 Oct 2024 14:08:02 +0200
Subject: [PATCH 05/11] Improve Version parsing, to better support 32 bit
values in older Version fields, instead of alwasy assuming 64 bit.
---
.../Utils/LsxXmlParsing/LsxXmlFormat.cs | 79 +++++++++++++------
.../Utils/LsxXmlParsing/MetaFileParser.cs | 22 +++---
2 files changed, 69 insertions(+), 32 deletions(-)
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 f37388c12..c321aeb3f 100644
--- a/src/Games/NexusMods.Games.Larian/BaldursGate3/Utils/LsxXmlParsing/LsxXmlFormat.cs
+++ b/src/Games/NexusMods.Games.Larian/BaldursGate3/Utils/LsxXmlParsing/LsxXmlFormat.cs
@@ -41,42 +41,77 @@ public struct ModuleVersion
public ulong Patch;
public ulong Build;
- public static ModuleVersion FromInt64(UInt64 packed)
+
+ public static ModuleVersion FromInt32String(string? str)
{
- return new ModuleVersion
+ if (string.IsNullOrWhiteSpace(str))
{
- Major = packed >> 55,
- Minor = (packed >> 47) & 0xFF,
- Patch = (packed >> 31) & 0xFFFF,
- Build = packed & 0x7FFFFFFFUL,
- };
- }
+ return FromUInt32(0);
+ }
- public static UInt64 ParseVersion(string? str)
+ if (!UInt32.TryParse(str, out var parse32Result))
+ {
+ // 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)
{
- // Even though version is marked as Int32, it could actually contain 64-bit values, so we need to parse it as UInt64
- if (string.IsNullOrWhiteSpace(str) || !UInt64.TryParse(str, out var result))
- return 0;
+ if (string.IsNullOrWhiteSpace(str) || !UInt64.TryParse(str, out var parse64Result))
+ {
+ return FromUInt64(0);
+ }
- if (result == 1 || result == 268435456)
+ return FromUInt64(parse64Result);
+ }
+
+ private static ModuleVersion FromUInt64(ulong uIntVal)
+ {
+ if (uIntVal == 1 || uIntVal == 268435456)
{
// v1.0.0.0
- return 36028797018963968;
+ return new ModuleVersion
+ {
+ Major = 1,
+ Minor = 0,
+ Patch = 0,
+ Build = 0,
+ };
}
- return result;
+
+ return new ModuleVersion
+ {
+ Major = uIntVal >> 55,
+ Minor = (uIntVal >> 47) & 0xFF,
+ Patch = (uIntVal >> 31) & 0xFFFF,
+ Build = uIntVal & 0x7FFFFFFFUL,
+ };
}
+
- public static UInt64 ParseVersion64(string? str)
+ private static ModuleVersion FromUInt32(UInt32 uIntVal)
{
- if (string.IsNullOrWhiteSpace(str) || !UInt64.TryParse(str, out var result))
- return 0;
-
- if (result == 1)
+ if (uIntVal == 1) // || uIntVal == 268435456)
{
// v1.0.0.0
- return 36028797018963968;
+ return new ModuleVersion
+ {
+ Major = 1,
+ Minor = 0,
+ Patch = 0,
+ Build = 0,
+ };
}
- return result;
+
+ return new ModuleVersion
+ {
+ Major = uIntVal >> 28,
+ Minor = (uIntVal >> 24) & 0x0F,
+ Patch = (uIntVal >> 16) & 0xFF,
+ Build = uIntVal & 0xFFFF,
+ };
}
public override string ToString() => $"{Major}.{Minor}.{Patch}";
diff --git a/src/Games/NexusMods.Games.Larian/BaldursGate3/Utils/LsxXmlParsing/MetaFileParser.cs b/src/Games/NexusMods.Games.Larian/BaldursGate3/Utils/LsxXmlParsing/MetaFileParser.cs
index 2de591350..e91b0c6ed 100644
--- a/src/Games/NexusMods.Games.Larian/BaldursGate3/Utils/LsxXmlParsing/MetaFileParser.cs
+++ b/src/Games/NexusMods.Games.Larian/BaldursGate3/Utils/LsxXmlParsing/MetaFileParser.cs
@@ -46,14 +46,15 @@ public static LsxXmlFormat.MetaFileData ParseMetaFile(Stream xmlStream)
case "PublishHandle":
moduleShortDesc.PublishHandle = value ?? string.Empty;
break;
- case "Version64":
+ case "Version":
+ // From older meta files, it seems that it could contain either 32-bit or 64-bit values even though type is marked as Int32
+ // They later changed it to Version64, to clarify the type, but older mods can still have this
moduleShortDesc.Version = value ?? string.Empty;
- moduleShortDesc.SemanticVersion = LsxXmlFormat.ModuleVersion.FromInt64(LsxXmlFormat.ModuleVersion.ParseVersion64(moduleShortDesc.Version));
+ moduleShortDesc.SemanticVersion = LsxXmlFormat.ModuleVersion.FromInt32String(moduleShortDesc.Version);
break;
- case "Version":
- // From older meta files, it seems that it could contain 64-bit values even though type is marked as Int32
+ case "Version64":
moduleShortDesc.Version = value ?? string.Empty;
- moduleShortDesc.SemanticVersion = LsxXmlFormat.ModuleVersion.FromInt64(LsxXmlFormat.ModuleVersion.ParseVersion(moduleShortDesc.Version));
+ moduleShortDesc.SemanticVersion = LsxXmlFormat.ModuleVersion.FromInt64String(moduleShortDesc.Version);
break;
case "UUID":
moduleShortDesc.Uuid = value ?? string.Empty;
@@ -106,14 +107,15 @@ public static LsxXmlFormat.MetaFileData ParseMetaFile(Stream xmlStream)
case "PublishHandle":
dependency.PublishHandle = value ?? string.Empty;
break;
- case "Version64":
+ case "Version":
+ // From older meta files, it seems that it could contain either 32-bit or 64-bit values even though type is marked as Int32
+ // They later changed it to Version64, to clarify the type, but older mods can still have this
dependency.Version = value ?? string.Empty;
- dependency.SemanticVersion = LsxXmlFormat.ModuleVersion.FromInt64(LsxXmlFormat.ModuleVersion.ParseVersion64(dependency.Version));
+ dependency.SemanticVersion = LsxXmlFormat.ModuleVersion.FromInt32String(dependency.Version);
break;
- case "Version":
- // From older meta files, it seems that it could contain 64-bit values even though type is marked as Int32
+ case "Version64":
dependency.Version = value ?? string.Empty;
- dependency.SemanticVersion = LsxXmlFormat.ModuleVersion.FromInt64(LsxXmlFormat.ModuleVersion.ParseVersion(dependency.Version));
+ dependency.SemanticVersion = LsxXmlFormat.ModuleVersion.FromInt64String(dependency.Version);
break;
case "UUID":
dependency.Uuid = value ?? string.Empty;
From f0795ce07443569c2fc74d6a9b995b4ac5a64c8d Mon Sep 17 00:00:00 2001
From: AL <26797547+Al12rs@users.noreply.github.com>
Date: Tue, 15 Oct 2024 14:34:24 +0200
Subject: [PATCH 06/11] Test cleanup
---
.../BaldursGate3/BG3PakParsingTests.cs | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/tests/Games/NexusMods.Games.Larian.Tests/BaldursGate3/BG3PakParsingTests.cs b/tests/Games/NexusMods.Games.Larian.Tests/BaldursGate3/BG3PakParsingTests.cs
index a94f60aac..2bf0fc3a9 100644
--- a/tests/Games/NexusMods.Games.Larian.Tests/BaldursGate3/BG3PakParsingTests.cs
+++ b/tests/Games/NexusMods.Games.Larian.Tests/BaldursGate3/BG3PakParsingTests.cs
@@ -40,7 +40,8 @@ public async Task ParsePakMeta_ShouldParseCorrectly(string pakFilePath)
sb.AppendLine("Dependency:");
sb.Append(LsxXmlFormat.SerializeModuleShortDesc(dependency));
sb.AppendLineN();
- sb.AppendLine($"SemanticVersion: {dependency.SemanticVersion.Major}.{dependency.SemanticVersion.Minor}.{dependency.SemanticVersion.Patch}.{dependency.SemanticVersion.Build}");
+ var ver = dependency.SemanticVersion;
+ sb.AppendLine($"SemanticVersion: {ver.Major}.{ver.Minor}.{ver.Patch}.{ver.Build}");
}
await Verify(sb.ToString()).UseParameters(pakFilePath);
}
From 381b4c736073e081afc89c6866d4bebcd9aa2948 Mon Sep 17 00:00:00 2001
From: AL <26797547+Al12rs@users.noreply.github.com>
Date: Tue, 15 Oct 2024 17:52:49 +0200
Subject: [PATCH 07/11] Add health check for BG3 Outdated pak dependencies
---
.../BaldursGate3/Diagnostics.cs | 38 +++++++---
.../Emitters/DependencyDiagnosticEmitter.cs | 69 ++++++++++++++-----
.../Utils/LsxXmlParsing/LsxXmlFormat.cs | 44 ++++++++++--
3 files changed, 120 insertions(+), 31 deletions(-)
diff --git a/src/Games/NexusMods.Games.Larian/BaldursGate3/Diagnostics.cs b/src/Games/NexusMods.Games.Larian/BaldursGate3/Diagnostics.cs
index b3413bd18..923011bd8 100644
--- a/src/Games/NexusMods.Games.Larian/BaldursGate3/Diagnostics.cs
+++ b/src/Games/NexusMods.Games.Larian/BaldursGate3/Diagnostics.cs
@@ -18,12 +18,12 @@ internal static partial class Diagnostics
.WithId(new DiagnosticId(Source, number: 1))
.WithTitle("Missing required dependency")
.WithSeverity(DiagnosticSeverity.Warning)
- .WithSummary("The mod {ModName} is missing the required dependency '{MissingDepName}' v{MissingDepVersion}.")
+ .WithSummary("The mod {ModName} is missing the required dependency '{MissingDepName}' v{MissingDepVersion}+.")
.WithDetails("""
- '{MissingDepName}' v{MissingDepVersion} is not installed or enabled in your Loadout. This pak module is required by '{PakModuleName}' v{PakModuleVersion} to run correctly.
+ '{MissingDepName}' v{MissingDepVersion}+ is not installed or enabled in your Loadout. This pak module is required by '{PakModuleName}' v{PakModuleVersion} to run correctly.
- ## Recommended actions
+ ## Recommended Actions
#### Search for and install the missing mod
You can search for '{MissingDepName}' on {NexusModsLink} or search the in-game mod manager.
#### Or
@@ -40,6 +40,28 @@ internal static partial class Diagnostics
)
.Finish();
+ [DiagnosticTemplate]
+ [UsedImplicitly]
+ internal static IDiagnosticTemplate OutdatedDependencyTemplate = DiagnosticTemplateBuilder
+ .Start()
+ .WithId(new DiagnosticId(Source, number: 1))
+ .WithTitle("Required dependency is outdated")
+ .WithSeverity(DiagnosticSeverity.Warning)
+ .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")
+ .AddValue("PakModuleVersion")
+ .AddDataReference("DepModName")
+ .AddValue("DepName")
+ .AddValue("MinDepVersion")
+ .AddValue("CurrentDepVersion")
+ )
+ .Finish();
+
[DiagnosticTemplate]
[UsedImplicitly]
@@ -48,14 +70,14 @@ internal static partial class Diagnostics
.WithId(new DiagnosticId(Source, number: 1))
.WithTitle("Invalid pak file")
.WithSeverity(DiagnosticSeverity.Warning)
- .WithSummary("The file '{PakFileName}' in mod {ModName} was not recognized as a valid Pak file.")
+ .WithSummary("Invalid .pak File Detected in {ModName}")
.WithDetails("""
- The app was unable to recognize the file '{PakFileName}' in mod {ModName} as a valid Pak file.
- This file is unlikely to work correctly in the game.
+ The mod contains a .pak file, typically used to store mod data for Baldur's Gate 3. However,
+ this one appears to be invalid or incompatible: '{PakFileName}'.
- ## Recommended actions
- Reinstall the mod or remove the file.
+ ## 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")
diff --git a/src/Games/NexusMods.Games.Larian/BaldursGate3/Emitters/DependencyDiagnosticEmitter.cs b/src/Games/NexusMods.Games.Larian/BaldursGate3/Emitters/DependencyDiagnosticEmitter.cs
index dba6fa9dd..3b12001e0 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.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;
@@ -51,41 +52,73 @@ private async Task> DiagnosePakModulesAsync(Loadout.Read
{
var (mod, metadataOrError) = metaFileTuple;
var loadoutItemGroup = mod.AsLoadoutItemWithTargetPath().AsLoadoutItem().Parent;
-
+
// error case
if (metadataOrError.IsT1)
{
diagnostics.Add(Diagnostics.CreateInvalidPakFile(
- ModName: loadoutItemGroup.ToReference(loadout),
- PakFileName: mod.AsLoadoutItemWithTargetPath().TargetPath.Item3.FileName
- ));
+ ModName: loadoutItemGroup.ToReference(loadout),
+ PakFileName: mod.AsLoadoutItemWithTargetPath().TargetPath.Item3.FileName
+ )
+ );
continue;
}
-
+
// non error case
var metadata = metadataOrError.AsT0;
var dependencies = metadata.Dependencies;
-
foreach (var dependency in dependencies)
{
var dependencyUuid = dependency.Uuid;
+ if (dependencyUuid == string.Empty)
+ continue;
+
+ var matchingDeps = metaFileTuples.Where(
+ x =>
+ x.Item2.IsT0 &&
+ x.Item2.AsT0.ModuleShortDesc.Uuid == dependencyUuid
+ )
+ .ToArray();
- if (dependencyUuid == string.Empty || metaFileTuples.Any(x => x.Item2.IsT0 && x.Item2.AsT0.ModuleShortDesc.Uuid == dependencyUuid))
+ if (matchingDeps.Length == 0)
{
+ // Missing dependency
+ diagnostics.Add(Diagnostics.CreateMissingDependency(
+ ModName: loadoutItemGroup.ToReference(loadout),
+ MissingDepName: dependency.Name,
+ MissingDepVersion: dependency.SemanticVersion.ToString(),
+ PakModuleName: metadata.ModuleShortDesc.Name,
+ PakModuleVersion: metadata.ModuleShortDesc.SemanticVersion.ToString(),
+ NexusModsLink: NexusModsLink
+ )
+ );
continue;
}
- // add diagnostic
- diagnostics.Add(Diagnostics.CreateMissingDependency(
- ModName: loadoutItemGroup.ToReference(loadout),
- MissingDepName: dependency.Name,
- MissingDepVersion: dependency.SemanticVersion.ToString(),
- PakModuleName: metadata.ModuleShortDesc.Name,
- PakModuleVersion: metadata.ModuleShortDesc.SemanticVersion.ToString(),
- NexusModsLink: NexusModsLink
- )
+ if (dependency.SemanticVersion == default(LsxXmlFormat.ModuleVersion))
+ continue;
+
+ var highestInstalledMatch = matchingDeps.MaxBy(
+ x => x.Item2.AsT0.ModuleShortDesc.SemanticVersion
);
+ var installedMatchModule = highestInstalledMatch.Item2.AsT0.ModuleShortDesc;
+ var matchLoadoutItemGroup = highestInstalledMatch.Item1.AsLoadoutItemWithTargetPath().AsLoadoutItem().Parent;
+
+ // Check if found dependency is outdated
+ if (installedMatchModule.SemanticVersion < dependency.SemanticVersion)
+ {
+ diagnostics.Add(Diagnostics.CreateOutdatedDependency(
+ ModName: loadoutItemGroup.ToReference(loadout),
+ PakModuleName: metadata.ModuleShortDesc.Name,
+ PakModuleVersion: metadata.ModuleShortDesc.SemanticVersion.ToString(),
+ DepModName: matchLoadoutItemGroup.ToReference(loadout),
+ DepName: installedMatchModule.Name,
+ MinDepVersion: dependency.SemanticVersion.ToString(),
+ CurrentDepVersion: installedMatchModule.SemanticVersion.ToString()
+ )
+ );
+ }
}
}
@@ -114,13 +147,13 @@ private async Task> DiagnosePakModulesAsync(Loadout.Read
logger.LogError(e, "Exception while getting metadata for `{Name}`", pakLoadoutFile.AsLoadoutItemWithTargetPath().TargetPath.Item3);
continue;
}
-
+
// Log the InvalidDataException case, but still return the resource
if (resource.Data.IsT1)
{
logger.LogWarning(resource.Data.AsT1.Value, "Detected invalid BG3 Pak file: `{Name}`", pakLoadoutFile.AsLoadoutItemWithTargetPath().TargetPath.Item3);
}
-
+
yield return (pakLoadoutFile, resource.Data);
}
}
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 c321aeb3f..1204dcf48 100644
--- a/src/Games/NexusMods.Games.Larian/BaldursGate3/Utils/LsxXmlParsing/LsxXmlFormat.cs
+++ b/src/Games/NexusMods.Games.Larian/BaldursGate3/Utils/LsxXmlParsing/LsxXmlFormat.cs
@@ -34,14 +34,13 @@ public struct ModuleShortDesc
}
- public struct ModuleVersion
+ 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))
@@ -71,7 +70,6 @@ private static ModuleVersion FromUInt64(ulong uIntVal)
{
if (uIntVal == 1 || uIntVal == 268435456)
{
- // v1.0.0.0
return new ModuleVersion
{
Major = 1,
@@ -93,9 +91,8 @@ private static ModuleVersion FromUInt64(ulong uIntVal)
private static ModuleVersion FromUInt32(UInt32 uIntVal)
{
- if (uIntVal == 1) // || uIntVal == 268435456)
+ if (uIntVal == 1)
{
- // v1.0.0.0
return new ModuleVersion
{
Major = 1,
@@ -115,6 +112,43 @@ private static ModuleVersion FromUInt32(UInt32 uIntVal)
}
public override string ToString() => $"{Major}.{Minor}.{Patch}";
+
+ public int CompareTo(ModuleVersion other)
+ {
+ var majorComparison = Major.CompareTo(other.Major);
+ if (majorComparison != 0) return majorComparison;
+ var minorComparison = Minor.CompareTo(other.Minor);
+ if (minorComparison != 0) return minorComparison;
+ var patchComparison = Patch.CompareTo(other.Patch);
+ if (patchComparison != 0) return patchComparison;
+ return Build.CompareTo(other.Build);
+ }
+
+ public bool Equals(ModuleVersion other)
+ {
+ return Major == other.Major && Minor == other.Minor && Patch == other.Patch && Build == other.Build;
+ }
+
+ public override bool Equals(object? obj)
+ {
+ return obj is ModuleVersion other && Equals(other);
+ }
+
+ public override int GetHashCode()
+ {
+ return HashCode.Combine(Major,
+ Minor,
+ Patch,
+ 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;
+ public static bool operator <(ModuleVersion left, ModuleVersion right) => left.CompareTo(right) < 0;
+ public static bool operator >=(ModuleVersion left, ModuleVersion right) => left.CompareTo(right) >= 0;
+ public static bool operator <=(ModuleVersion left, ModuleVersion right) => left.CompareTo(right) <= 0;
}
From 8a36537871a8a1555bdef1b3be8a28dd37634f9f Mon Sep 17 00:00:00 2001
From: AL <26797547+Al12rs@users.noreply.github.com>
Date: Wed, 16 Oct 2024 11:32:55 +0200
Subject: [PATCH 08/11] Add space between NexuMods
---
src/Games/NexusMods.Games.Larian/BaldursGate3/Diagnostics.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/Games/NexusMods.Games.Larian/BaldursGate3/Diagnostics.cs b/src/Games/NexusMods.Games.Larian/BaldursGate3/Diagnostics.cs
index 923011bd8..0ddf47ef3 100644
--- a/src/Games/NexusMods.Games.Larian/BaldursGate3/Diagnostics.cs
+++ b/src/Games/NexusMods.Games.Larian/BaldursGate3/Diagnostics.cs
@@ -27,7 +27,7 @@ internal static partial class Diagnostics
#### Search for and install the missing mod
You can search for '{MissingDepName}' on {NexusModsLink} or search the in-game mod manager.
#### Or
- #### Check the required mods section on {ModName} NexusMods page
+ #### 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
From 932d125f14eef2883863483a1c427e37dd3f8899 Mon Sep 17 00:00:00 2001
From: AL <26797547+Al12rs@users.noreply.github.com>
Date: Wed, 16 Oct 2024 12:59:39 +0200
Subject: [PATCH 09/11] Update BG3 docs with info on Vulkan and DX11 as well as
Script Extender details
---
docs/developers/games/0003-BaldursGate3.md | 45 +++++++++++++++++++---
1 file changed, 40 insertions(+), 5 deletions(-)
diff --git a/docs/developers/games/0003-BaldursGate3.md b/docs/developers/games/0003-BaldursGate3.md
index 67f12fc6f..8129148bb 100644
--- a/docs/developers/games/0003-BaldursGate3.md
+++ b/docs/developers/games/0003-BaldursGate3.md
@@ -11,16 +11,37 @@ BG3 was released in Early Access in 2020, and thanks to the similarities with DO
BG3 has native Windows and MacOS support, but Linux users can play it using Wine.
-
## Game Files and Locations
### Windows/Wine:
-Two executables: `bg3.exe` and `bg3_dx11.exe` in `Baldurs Gate 3/Bin`. One for Vulkan, one for DirectX 11.
+Game launcher in `Baldurs Gate 3\Launcher\LariLauncher.exe`
+Two actual game executables: `bg3.exe` and `bg3_dx11.exe` in `Baldurs Gate 3/Bin`. One for Vulkan, one for DirectX 11.
Game settings and load order are stored in `%localappdata%\Larian Studios\Baldur's Gate 3`.
Majority of mods are stored in `%localappdata%\Larian Studios\Baldur's Gate 3\Mods`.
Load order is stored in `%localappdata%\Larian Studios\Baldur's Gate 3\PlayerProfiles\Public\modsettings.lsx`.
-
### MacOS:
-TBD
+Only one executable `Baldur's Gate 3.app/Contents/MacOS/Baldur's Gate 3`
+For more info on game folder: https://steamdb.info/depot/1419660/
+Equivalent for Appdata and Mods folder TBD.
+
+### Vulkan and DirectX11:
+On windows and Wine, the game Launcher allows users to choose between running the Vulkan or DirectX11 version of the game.
+Using one or the other can affect game performance depending on the system. Additionally, some mods such as Texture replacers, may require one or the other.
+
+It is important to allow users know and choose which version of the game they are running, as mods may not work correctly if the wrong version is used.
+Running the Launcher allows users to choose the version of the game to run at the cost of a longer startup time.
+
+The launcher supports the following command line arguments:
+- `--skip-launcher` Skips the launcher and launches the game with whatever was last selected.
+- `--vulkan` Launches the game with Vulkan.
+- `--dx11` Launches the game with DirectX11.
+
+The Launcher stores the last selected version in `AppData\Local\Larian Studios\Launcher\Settings\preferences.json`.
+If `"DefaultRenderingBackend": 0` exists, Vulkan is loaded, if the property is set to anything other than 0, or is missing, DirectX11 is loaded and the property is removed.
+
+On Steam running any of the executables will prompt the game to be launched from Steam instead, which will then in turn run the Launcher.
+
+On GOG, running the executables will launch the game directly, bypassing the Launcher, because there is no Steam-like DRM looping back to the Launcher.
+
## Mod formats:
### BG3 Script Extender (BG3SE)
@@ -267,7 +288,21 @@ Since there is no evident way to distinguish between vanilla and mod pak files,
## Essential Mods & Tools
- BG3SE
Requirement for a lot of mods, but not allowed for Modio mods.
-New scripting capabilities (osiris scripting) added in patch 7 may reduce the need for BG3SE in the future.
+Requires additional steps to work on Linux(Wine): https://wiki.bg3.community/en/Tutorials/Mod-Use/How-to-install-Script-Extender#h-3-install-script-extender-on-linuxsteam-deck
+
+Pak mods that use the Script Extender will contain a ScriptExtender folder with a `config.json` config file with a format similar to this:
+```json
+{
+ "RequiredVersion": 19,
+ "ModTable": "WaypointInsideEmeraldGrove",
+ "FeatureFlags": [
+ "Lua"
+ ]
+}
+```
+`RequiredVersion` indicates the minimum version of the Script Extender required for the mod to work.
+Details on schema: https://github.com/Norbyte/bg3se/blob/updater-20240329/Docs/API.md
+
For mod authors:
- BG3 Toolkit: https://store.steampowered.com/app/2956320/Baldurs_Gate_3_Toolkit_Data/
From 5573c29a744a62ce611bcf4c5ed36edacc983822 Mon Sep 17 00:00:00 2001
From: AL <26797547+Al12rs@users.noreply.github.com>
Date: Wed, 16 Oct 2024 13:01:27 +0200
Subject: [PATCH 10/11] Change BG3 Game to launch `LariLauncher.exe` rather
than Vulkan exe, to allow users to choose on GOG
---
src/Games/NexusMods.Games.Larian/BaldursGate3/BaldursGate3.cs | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/src/Games/NexusMods.Games.Larian/BaldursGate3/BaldursGate3.cs b/src/Games/NexusMods.Games.Larian/BaldursGate3/BaldursGate3.cs
index 6a8975c58..a7a83cab3 100644
--- a/src/Games/NexusMods.Games.Larian/BaldursGate3/BaldursGate3.cs
+++ b/src/Games/NexusMods.Games.Larian/BaldursGate3/BaldursGate3.cs
@@ -39,7 +39,9 @@ public override GamePath GetPrimaryFile(GameStore store)
{
if (_osInformation.IsOSX)
return new GamePath(LocationId.Game, "Contents/MacOS/Baldur's Gate 3");
- return new GamePath(LocationId.Game, "bin/bg3.exe");
+
+ // Use launcher to allow choosing between DirectX11 and Vulkan on GOG, Steam already always starts the launcher
+ return new GamePath(LocationId.Game, "Launcher/LariLauncher.exe");
}
protected override IReadOnlyDictionary GetLocations(IFileSystem fileSystem, GameLocatorResult installation)
From 3fe4c84ca854f3b3d9bdd39c57da767a687b46cb Mon Sep 17 00:00:00 2001
From: Al12rs <26797547+Al12rs@users.noreply.github.com>
Date: Mon, 21 Oct 2024 16:36:52 +0200
Subject: [PATCH 11/11] Use Outcome rather than OneOf
---
.../Emitters/DependencyDiagnosticEmitter.cs | 27 +++++++++----------
.../BaldursGate3/Pipelines.cs | 16 +++++------
.../NexusMods.Games.Larian.csproj | 1 +
3 files changed, 22 insertions(+), 22 deletions(-)
diff --git a/src/Games/NexusMods.Games.Larian/BaldursGate3/Emitters/DependencyDiagnosticEmitter.cs b/src/Games/NexusMods.Games.Larian/BaldursGate3/Emitters/DependencyDiagnosticEmitter.cs
index 3b12001e0..11cf68056 100644
--- a/src/Games/NexusMods.Games.Larian/BaldursGate3/Emitters/DependencyDiagnosticEmitter.cs
+++ b/src/Games/NexusMods.Games.Larian/BaldursGate3/Emitters/DependencyDiagnosticEmitter.cs
@@ -5,19 +5,18 @@
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.Hashing.xxHash64;
-using OneOf.Types;
+using Polly;
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)
{
@@ -54,7 +53,7 @@ private async Task> DiagnosePakModulesAsync(Loadout.Read
var loadoutItemGroup = mod.AsLoadoutItemWithTargetPath().AsLoadoutItem().Parent;
// error case
- if (metadataOrError.IsT1)
+ if (metadataOrError.Exception is not null)
{
diagnostics.Add(Diagnostics.CreateInvalidPakFile(
ModName: loadoutItemGroup.ToReference(loadout),
@@ -65,7 +64,7 @@ private async Task> DiagnosePakModulesAsync(Loadout.Read
}
// non error case
- var metadata = metadataOrError.AsT0;
+ var metadata = metadataOrError.Result;
var dependencies = metadata.Dependencies;
foreach (var dependency in dependencies)
@@ -76,8 +75,8 @@ private async Task> DiagnosePakModulesAsync(Loadout.Read
var matchingDeps = metaFileTuples.Where(
x =>
- x.Item2.IsT0 &&
- x.Item2.AsT0.ModuleShortDesc.Uuid == dependencyUuid
+ x.Item2.Exception is null &&
+ x.Item2.Result.ModuleShortDesc.Uuid == dependencyUuid
)
.ToArray();
@@ -100,9 +99,9 @@ private async Task> DiagnosePakModulesAsync(Loadout.Read
continue;
var highestInstalledMatch = matchingDeps.MaxBy(
- x => x.Item2.AsT0.ModuleShortDesc.SemanticVersion
+ x => x.Item2.Result.ModuleShortDesc.SemanticVersion
);
- var installedMatchModule = highestInstalledMatch.Item2.AsT0.ModuleShortDesc;
+ var installedMatchModule = highestInstalledMatch.Item2.Result.ModuleShortDesc;
var matchLoadoutItemGroup = highestInstalledMatch.Item1.AsLoadoutItemWithTargetPath().AsLoadoutItem().Parent;
// Check if found dependency is outdated
@@ -129,15 +128,15 @@ private async Task> DiagnosePakModulesAsync(Loadout.Read
#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);
@@ -149,9 +148,9 @@ private async Task> DiagnosePakModulesAsync(Loadout.Read
}
// Log the InvalidDataException case, but still return the resource
- if (resource.Data.IsT1)
+ if (resource.Data.Exception is not null)
{
- logger.LogWarning(resource.Data.AsT1.Value, "Detected invalid BG3 Pak file: `{Name}`", pakLoadoutFile.AsLoadoutItemWithTargetPath().TargetPath.Item3);
+ logger.LogWarning(resource.Data.Exception, "Detected invalid BG3 Pak file: `{Name}`", pakLoadoutFile.AsLoadoutItemWithTargetPath().TargetPath.Item3);
}
yield return (pakLoadoutFile, resource.Data);
diff --git a/src/Games/NexusMods.Games.Larian/BaldursGate3/Pipelines.cs b/src/Games/NexusMods.Games.Larian/BaldursGate3/Pipelines.cs
index 8e759be90..5adbbe29e 100644
--- a/src/Games/NexusMods.Games.Larian/BaldursGate3/Pipelines.cs
+++ b/src/Games/NexusMods.Games.Larian/BaldursGate3/Pipelines.cs
@@ -8,7 +8,7 @@
using NexusMods.Games.Larian.BaldursGate3.Utils.LsxXmlParsing;
using NexusMods.Games.Larian.BaldursGate3.Utils.PakParsing;
using NexusMods.Hashing.xxHash64;
-using OneOf.Types;
+using Polly;
namespace NexusMods.Games.Larian.BaldursGate3;
@@ -18,7 +18,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,27 +26,27 @@ 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)
- .ThenDo>, Stream, Unit>(Unit.Default,
+ .ThenDo(Unit.Default,
static (_, _, resource, _) =>
{
try
{
var metaFileData = PakFileParser.ParsePakMeta(resource.Data);
- return ValueTask.FromResult(resource.WithData(OneOf.OneOf>.FromT0(metaFileData)));
+ return ValueTask.FromResult(resource.WithData(Outcome.FromResult(metaFileData)));
}
catch (InvalidDataException e)
{
- return ValueTask.FromResult(resource.WithData(OneOf.OneOf>.FromT1(new Error(e))));
+ return ValueTask.FromResult(resource.WithData(Outcome.FromException(e)));
}
}
)
diff --git a/src/Games/NexusMods.Games.Larian/NexusMods.Games.Larian.csproj b/src/Games/NexusMods.Games.Larian/NexusMods.Games.Larian.csproj
index dbc487eaa..25ead10d2 100644
--- a/src/Games/NexusMods.Games.Larian/NexusMods.Games.Larian.csproj
+++ b/src/Games/NexusMods.Games.Larian/NexusMods.Games.Larian.csproj
@@ -25,6 +25,7 @@
+