Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BG3: Add support for serializing and deserializing modsettings.lsx Load Order file #2149

Merged
merged 3 commits into from
Oct 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

namespace NexusMods.Games.Larian.BaldursGate3.Utils.PakParsing;

/// <summary>
/// Static class containing definitions for the Larian Package (LSPK, `.pak`) format.
/// </summary>
public static class LspkPackageFormat
{
#region Enums
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
using System.Text;
using System.Xml;

namespace NexusMods.Games.Larian.BaldursGate3.Utils.PakParsing;

/// <summary>
/// Class containing definitions for the Larian Xml (LSX) format.
/// </summary>
public static class LsxXmlFormat
{

Expand Down Expand Up @@ -35,32 +39,19 @@ public struct ModuleShortDesc
/// </summary>
public static string SerializeModuleShortDesc(ModuleShortDesc moduleShortDesc)
{
var settings = new XmlWriterSettings
{
Indent = true,
OmitXmlDeclaration = true,
};

using var stringWriter = new StringWriter();
using (var xmlWriter = XmlWriter.Create(stringWriter, new XmlWriterSettings { Indent = true, OmitXmlDeclaration = true}))
using (var xmlWriter = XmlWriter.Create(stringWriter, settings))
{
xmlWriter.WriteStartElement("node");
xmlWriter.WriteAttributeString("id", "ModuleShortDesc");

WriteAttribute(xmlWriter, "Folder", "LSString", moduleShortDesc.Folder);
WriteAttribute(xmlWriter, "MD5", "LSString", moduleShortDesc.Md5);
WriteAttribute(xmlWriter, "Name", "LSString", moduleShortDesc.Name);
WriteAttribute(xmlWriter, "PublishHandle", "uint64", moduleShortDesc.PublishHandle);
WriteAttribute(xmlWriter, "UUID", "guid", moduleShortDesc.Uuid);
WriteAttribute(xmlWriter, "Version64", "int64", moduleShortDesc.Version);

xmlWriter.WriteEndElement();
ModsettingsFileFormat.WriteModuleShortDesc(xmlWriter, moduleShortDesc);
}

return stringWriter.ToString();

static void WriteAttribute(XmlWriter xmlWriter, string id, string type, string value)
{
xmlWriter.WriteStartElement("attribute");
xmlWriter.WriteAttributeString("id", id);
xmlWriter.WriteAttributeString("type", type);
xmlWriter.WriteAttributeString("value", value);
xmlWriter.WriteEndElement();
}
}


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
using System.Text;
using System.Xml;

namespace NexusMods.Games.Larian.BaldursGate3.Utils.PakParsing;

/// <summary>
/// Class to parse and write the `modsettings.lsx` xml file format used for the BG3 Load Order.
/// </summary>
public static class ModsettingsFileFormat
{
public static string SerializeModsettingsLoadOrder(LsxXmlFormat.ModuleShortDesc[] moduleShortDescs)
{
var settings = new XmlWriterSettings
{
Indent = true,
OmitXmlDeclaration = false,
};

using var stringWriter = new StringWriter();
using (var xmlWriter = XmlWriter.Create(stringWriter, settings))
{
xmlWriter.WriteStartDocument();
xmlWriter.WriteStartElement("save");

WriteVersion(xmlWriter);
WriteRegion(xmlWriter, moduleShortDescs);

xmlWriter.WriteEndElement(); // save
xmlWriter.WriteEndDocument();
}

return stringWriter.ToString();
}

public static LsxXmlFormat.ModuleShortDesc[] DeserializeModsettingsLoadOrder(string modsettingsXml)
{
var modulesLoadOrder = new List<LsxXmlFormat.ModuleShortDesc>();
var skipFirstGustavDev = true;

using (var stringReader = new StringReader(modsettingsXml))
using (var xmlReader = XmlReader.Create(stringReader))
{
while (xmlReader.Read())
{
if (xmlReader is not { NodeType: XmlNodeType.Element, Name: "node" } ||
xmlReader.GetAttribute("id") != "ModuleShortDesc")
{
continue;
}

var moduleShortDesc = new LsxXmlFormat.ModuleShortDesc();

while (xmlReader.Read() && xmlReader is not { NodeType: XmlNodeType.EndElement, Name: "node" })
{
if (xmlReader is { NodeType: XmlNodeType.Element, Name: "attribute" })
{
var id = xmlReader.GetAttribute("id");
var value = xmlReader.GetAttribute("value");

switch (id)
{
case "Folder":
moduleShortDesc.Folder = value ?? string.Empty;
break;
case "MD5":
moduleShortDesc.Md5 = value ?? string.Empty;
break;
case "Name":
moduleShortDesc.Name = value ?? string.Empty;
break;
case "PublishHandle":
moduleShortDesc.PublishHandle = value ?? string.Empty;
break;
case "UUID":
moduleShortDesc.Uuid = value ?? string.Empty;
break;
case "Version64":
moduleShortDesc.Version = value ?? string.Empty;
break;
}
}
}

// Skip the GustavDev entry
if (skipFirstGustavDev && moduleShortDesc is { Name: "GustavDev" })
{
skipFirstGustavDev = false;
continue;
}

modulesLoadOrder.Add(moduleShortDesc);
}
}

return modulesLoadOrder.ToArray();
}

#region private methods

private static void WriteVersion(XmlWriter xmlWriter)
{
xmlWriter.WriteStartElement("version");
xmlWriter.WriteAttributeString("major", "4");
xmlWriter.WriteAttributeString("minor", "7");
xmlWriter.WriteAttributeString("revision", "1");
xmlWriter.WriteAttributeString("build", "200");
xmlWriter.WriteEndElement();
}

private static void WriteRegion(XmlWriter xmlWriter, LsxXmlFormat.ModuleShortDesc[] moduleShortDescs)
{
xmlWriter.WriteStartElement("region");
xmlWriter.WriteAttributeString("id", "ModuleSettings");

xmlWriter.WriteStartElement("node");
xmlWriter.WriteAttributeString("id", "root");

xmlWriter.WriteStartElement("children");
xmlWriter.WriteStartElement("node");
xmlWriter.WriteAttributeString("id", "Mods");

xmlWriter.WriteStartElement("children");

// Add default GustavDev entry
WriteModuleShortDesc(xmlWriter,
new LsxXmlFormat.ModuleShortDesc
{
Folder = "GustavDev",
Name = "GustavDev",
PublishHandle = "0",
Version = "36028797018963968",
Uuid = "28ac9ce2-2aba-8cda-b3b5-6e922f71b6b8",
Md5 = ""
}
);

// Add pak mod entries
foreach (var moduleShortDesc in moduleShortDescs)
{
WriteModuleShortDesc(xmlWriter, moduleShortDesc);
}

xmlWriter.WriteEndElement(); // children
xmlWriter.WriteEndElement(); // node Mods
xmlWriter.WriteEndElement(); // children
xmlWriter.WriteEndElement(); // node root
xmlWriter.WriteEndElement(); // region
}

internal static void WriteModuleShortDesc(XmlWriter xmlWriter, LsxXmlFormat.ModuleShortDesc moduleShortDesc)
{
xmlWriter.WriteStartElement("node");
xmlWriter.WriteAttributeString("id", "ModuleShortDesc");

WriteAttribute(xmlWriter,
"Folder",
"LSString",
moduleShortDesc.Folder
);
WriteAttribute(xmlWriter,
"MD5",
"LSString",
moduleShortDesc.Md5
);
WriteAttribute(xmlWriter,
"Name",
"LSString",
moduleShortDesc.Name
);
WriteAttribute(xmlWriter,
"PublishHandle",
"uint64",
moduleShortDesc.PublishHandle
);
WriteAttribute(xmlWriter,
"UUID",
"guid",
moduleShortDesc.Uuid
);
WriteAttribute(xmlWriter,
"Version64",
"int64",
moduleShortDesc.Version
);

xmlWriter.WriteEndElement();
}

private static void WriteAttribute(XmlWriter xmlWriter, string id, string type, string value)
{
xmlWriter.WriteStartElement("attribute");
xmlWriter.WriteAttributeString("id", id);
xmlWriter.WriteAttributeString("type", type);
xmlWriter.WriteAttributeString("value", value);
xmlWriter.WriteEndElement();
}

#endregion private methods
}
Loading
Loading