Skip to content
Merged
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
43d0e25
Fix: Don't use invalid array indices when parsing openmw data directo…
magicaldave Jun 10, 2025
d55b72e
FIX: Ignore commented lines during openmw config parsing
magicaldave Jun 10, 2025
4b24e25
FIX: Only replace forward slashes with backslashes on platforms where…
magicaldave Jun 10, 2025
5961794
FIX: Actually use the configPath parameter passed to OpenMWInstallati…
magicaldave Jun 10, 2025
9722d47
FIX: Don't trim the first character off of entries which don't start …
magicaldave Jun 10, 2025
f629bfb
FIX: Properly handle quoted setting values in openmw.cfg
magicaldave Jun 10, 2025
e0a0163
CLEANUP: Slightly more polite separator replacement
magicaldave Jun 10, 2025
944691d
FEAT: Absolutize paths in openmw.cfg
magicaldave Jun 10, 2025
c728a44
FEAT: Handle the data-local directory during config parsing
magicaldave Jun 10, 2025
54d27fd
FIX: In the case of parsing a local or global openmw.cfg, create a da…
magicaldave Jun 10, 2025
c8c23e7
FEAT: Refactor GetConfigurationLocation to return the default config …
magicaldave Jun 10, 2025
51f96a0
CLEANUP: Fix compile errors, rename some variables, and correctly han…
magicaldave Jun 10, 2025
71b1763
FIX: Interpret the last double-quote without a trailing ampersand as …
magicaldave Jun 11, 2025
1b32b3c
CLEANUP: data-local is not really its own thing by default but instea…
magicaldave Jun 11, 2025
45cdae0
FIX: Handle data-local parsing the same way as all other data directo…
magicaldave Jun 11, 2025
290a46e
CLEANUP: Don't special-case ?userconfig? and instead handle `config` …
magicaldave Jun 11, 2025
854c288
FEAT: Correctly handle token usage in data directories (mostly)
magicaldave Jun 11, 2025
7310d6f
FIX: Token replacement should actually occur after quote handling
magicaldave Jun 11, 2025
ab72da2
FEAT: Also trim duplicate and trailing path separators from anything …
magicaldave Jun 11, 2025
2487524
CLEANUP: Only parse the data-local directory and verify its existence…
magicaldave Jun 11, 2025
9c1bf13
FEAT: Handle the resources directory since we did the other things an…
magicaldave Jun 11, 2025
c86b598
CLEANUP: Prevent log spam by skipping omwscripts files
magicaldave Jun 11, 2025
9627b1a
FIX: Don't attempt to load directories which are config keys but don'…
magicaldave Jun 11, 2025
dac1b90
CLEANUP: Throw on duplicate content files
magicaldave Jun 11, 2025
2cce684
CLEANUP: Fix spacing in log output when a sub-configuration isn't loaded
magicaldave Jun 11, 2025
c60155b
CLEANUP: I guess I did badly overcomplicate that, didn't I?
magicaldave Jun 11, 2025
3c38bea
CLEANUP: This is probably not actually necessary but it will keep me …
magicaldave Jun 11, 2025
179ad37
FIX: Process child configurations only after the curen one has finished.
magicaldave Jun 11, 2025
dcb2b57
FIX: ParseDataDirectory must be called immediately when adding the da…
magicaldave Jun 11, 2025
a86dba2
CLEANUP: Imrpove logging on failures
magicaldave Jun 11, 2025
a4b1660
FIX: Correct a bug in token replacement where we never actually neede…
magicaldave Jun 11, 2025
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
125 changes: 113 additions & 12 deletions TES3Merge/Util/Installation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using IniParser.Model;
using Microsoft.Win32;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using TES3Merge.BSA;

namespace TES3Merge.Util;
Expand Down Expand Up @@ -260,7 +261,8 @@ private void BuildArchiveList()
for (var i = 0; true; ++i)
{
var archive = configArchives["Archive " + i];
if (string.IsNullOrEmpty(archive)) {
if (string.IsNullOrEmpty(archive))
{
break;
}
Archives.Add(archive);
Expand Down Expand Up @@ -381,60 +383,159 @@ public override string GetDefaultOutputDirectory()
public class OpenMWInstallation : Installation
{
private List<string> DataDirectories = new();
private string? DataLocalDirectory;
private string? ResourcesDirectory;
private static readonly string DuplicateSeparatorPattern =
$"[{Regex.Escape($"{Path.DirectorySeparatorChar}{Path.AltDirectorySeparatorChar}")}]+";

public OpenMWInstallation(string path) : base(path)
{
LoadConfiguration();
LoadConfiguration(path);

if (!string.IsNullOrEmpty(DataLocalDirectory))
{
DataDirectories.Add(DataLocalDirectory);

if (!Directory.Exists(DataLocalDirectory))
Directory.CreateDirectory(DataLocalDirectory);
}

if (!string.IsNullOrEmpty(ResourcesDirectory))
DataDirectories.Insert(0, Path.Combine(ParseDataDirectory(path, ResourcesDirectory), "vfs"));
}

private static string GetConfigurationLocation()
private static string GetDefaultConfigurationDirectory()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
var myDocs = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
return Path.Combine(myDocs, "My Games", "OpenMW", "openmw.cfg");
return Path.Combine(myDocs, "My Games", "OpenMW");
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
return Path.Combine(home, ".config", "openmw", "openmw.cfg");
return Path.Combine(home, ".config", "openmw");
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
return Path.Combine(home, "Library", "Preferences", "openmw", "openmw.cfg");
return Path.Combine(home, "Library", "Preferences", "openmw");
}

throw new Exception("Could not determine configuration path.");
}

private void LoadConfiguration()
private static string GetDefaultUserDataDirectory()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
var myDocs = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
return Path.Combine(myDocs, "My Games", "OpenMW");
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
var dataHome = Environment.GetEnvironmentVariable("XDG_DATA_HOME");

if (string.IsNullOrEmpty(dataHome))
dataHome = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local", "share");

return Path.Combine(dataHome, "openmw");
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
return Path.Combine(home, "Library", "Application Support", "openmw");
}

throw new Exception("Could not determine user data directory.");
}

private static string ParseDataDirectory(string configDir, string dataDir)
{
if (dataDir.StartsWith('"'))
{
var original = dataDir;
dataDir = "";
for (int i = 1; i < original.Length; i++)
{
if (original[i] == '&')
i++;
else if (original[i] == '"')
break;
dataDir += original[i];
}
}

if (dataDir.StartsWith("?userdata?"))
dataDir = dataDir.Replace("?userdata?", GetDefaultUserDataDirectory());
else if (dataDir.StartsWith("?userconfig?"))
dataDir = dataDir.Replace("?userconfig?", GetDefaultConfigurationDirectory());

if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
dataDir = dataDir.Replace('/', '\\');

if (!Path.IsPathRooted(dataDir))
dataDir = Path.GetFullPath(Path.Combine(configDir, dataDir));

return dataDir;
}

private void LoadConfiguration(string configDir)
{
var configPath = GetConfigurationLocation();
var configPath = Path.Combine(configDir, "openmw.cfg");
if (!File.Exists(configPath))
{
throw new Exception("Configuration file does not exist.");
throw new Exception("openmw.cfg does not exist at the path " + configPath);
}

List<string> subConfigs = new List<string> { };
foreach (var line in File.ReadLines(configPath))
{
if (string.IsNullOrEmpty(line) || line.Trim().StartsWith("#")) continue;

var tokens = line.Split('=', 2);

if (tokens.Length < 2) continue;

var key = tokens[0].Trim();
var value = tokens[1].Trim(new char[] { ' ', '"' });
var value = tokens[1].Trim();

switch (key)
{
case "data":
DataDirectories.Add(value.Replace('/', '\\'));
DataDirectories.Add(ParseDataDirectory(configDir, value));
break;
case "content":
if (value.ToLower().EndsWith(".omwscripts")) continue;
else if (GameFiles.Contains(value))
throw new Exception(value + " was listed as a content file by two configurations! The second one was: " + configDir);

GameFiles.Add(value);
break;
case "fallback-archive":
Archives.Add(value);
break;
case "data-local":
DataLocalDirectory = ParseDataDirectory(configDir, value);
break;
case "config":
subConfigs.Add(ParseDataDirectory(configDir, value));
break;
case "resources":
ResourcesDirectory = ParseDataDirectory(configDir, value);
break;
}
}

foreach (string config in subConfigs)
try
{
LoadConfiguration(ParseDataDirectory(configDir, config));
}
catch (Exception e)
{
Util.Logger.WriteLine("WARNING: Sub-configuration " + configDir + " does not contain an openmw.cfg, skipping due to: " + e);
}
}

/// <summary>
Expand Down Expand Up @@ -475,7 +576,7 @@ private void MapNormalFiles(string dataFiles)
.GetFiles(dataFiles, "*", SearchOption.AllDirectories)
.Where(x => !x.EndsWith(".mohidden"))
//.Where(x => !x.Contains(Path.DirectorySeparatorChar + ".git" + Path.DirectorySeparatorChar))
.Select(x => x[(dataFiles.Length + 1)..]);
.Select(x => Path.GetRelativePath(dataFiles, x));

foreach (var file in physicalFiles)
{
Expand Down
Loading