aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorSulfurNitride <SulfurNitride@users.noreply.github.com>2026-04-15 15:25:35 -0500
committerSulfurNitride <SulfurNitride@users.noreply.github.com>2026-04-15 15:25:35 -0500
commit6fe3cf9f6becc6853587c34998c94b9d54137051 (patch)
treef5c50c7c9e5cc0c95e3ae35a8b6155014a5cc1f6 /src
parent0770a18d6968c464f175eae1fa129b0a3fbf8438 (diff)
Mirror synced plugins across case variants, fix OMOD logging
syncPluginsBack previously updated the profile from the newest case variant but left the stale sibling (e.g. lowercase plugins.txt after LOOT edited Plugins.txt) untouched in the prefix, so anything reading the prefix before the next deployPlugins saw divergent content. Mirror the newest variant into every sibling after the profile sync. installer_omod.py called mobase.log / mobase.LogLevel which the Python bindings do not expose, crashing .omod installs with AttributeError. Route messages through stderr so the Python runner forwards them to MOBase::log::error. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'src')
-rw-r--r--src/plugins/installer_omod.py29
-rw-r--r--src/src/wineprefix.cpp72
2 files changed, 59 insertions, 42 deletions
diff --git a/src/plugins/installer_omod.py b/src/plugins/installer_omod.py
index 2a586c1..973b9ba 100644
--- a/src/plugins/installer_omod.py
+++ b/src/plugins/installer_omod.py
@@ -13,6 +13,7 @@ import lzma
import os
import shutil
import struct
+import sys
import tempfile
import zipfile
import zlib
@@ -21,6 +22,14 @@ from pathlib import Path
import mobase
+def _log_error(msg: str) -> None:
+ print(f"[OMOD] {msg}", file=sys.stderr)
+
+
+def _log_warn(msg: str) -> None:
+ print(f"[OMOD] WARNING: {msg}", file=sys.stderr)
+
+
class OmodInstaller(mobase.IPluginInstallerCustom):
_organizer: mobase.IOrganizer
@@ -79,7 +88,7 @@ class OmodInstaller(mobase.IPluginInstallerCustom):
try:
return self._do_install(mod_name, archive_name)
except Exception as e:
- mobase.log(mobase.LogLevel.ERROR, f"OMOD install failed: {e}")
+ _log_error(f"OMOD install failed: {e}")
return mobase.InstallResult.FAILED
def _do_install(
@@ -91,7 +100,7 @@ class OmodInstaller(mobase.IPluginInstallerCustom):
names = zf.namelist()
if "config" not in names:
- mobase.log(mobase.LogLevel.WARNING, "OMOD: no config entry found")
+ _log_warn("no config entry found")
return mobase.InstallResult.NOT_ATTEMPTED
config = self._parse_config(zf.read("config"))
@@ -127,9 +136,7 @@ class OmodInstaller(mobase.IPluginInstallerCustom):
file_list.append(rel)
if not file_list:
- mobase.log(
- mobase.LogLevel.WARNING, "OMOD: no files extracted"
- )
+ _log_warn("no files extracted")
return mobase.InstallResult.FAILED
# Repackage as a standard zip for MO2's installer.
@@ -155,10 +162,7 @@ class OmodInstaller(mobase.IPluginInstallerCustom):
if stream_name not in names:
return
if crc_name not in names:
- mobase.log(
- mobase.LogLevel.WARNING,
- f"OMOD: {stream_name} present but {crc_name} missing",
- )
+ _log_warn(f"{stream_name} present but {crc_name} missing")
return
file_list = self._parse_crc_file(zf.read(crc_name))
@@ -171,11 +175,10 @@ class OmodInstaller(mobase.IPluginInstallerCustom):
offset = 0
for path, size in file_list:
if offset + size > len(decompressed):
- mobase.log(
- mobase.LogLevel.WARNING,
- f"OMOD: truncated stream for {path} "
+ _log_warn(
+ f"truncated stream for {path} "
f"(need {size} bytes at offset {offset}, "
- f"have {len(decompressed)})",
+ f"have {len(decompressed)})"
)
break
diff --git a/src/src/wineprefix.cpp b/src/src/wineprefix.cpp
index 741855d..fa1d5e7 100644
--- a/src/src/wineprefix.cpp
+++ b/src/src/wineprefix.cpp
@@ -548,9 +548,22 @@ bool WinePrefix::syncPluginsBack(const QString& profilePluginsPath,
return true;
}
- auto pickNewest = [&](const QString& canonicalName) -> QString {
+ // Pick the newest case variant, sync it to the profile, then mirror its
+ // content back into every sibling variant in the prefix so they don't
+ // drift. LOOT (and similar tools) only edit whichever case variant they
+ // opened; without this mirror the untouched sibling keeps stale content
+ // until the next deployPlugins, which can confuse anything that reads
+ // the prefix before then.
+ auto syncOne = [&](const QString& canonicalName,
+ const QString& profilePath) -> bool {
const QStringList variants =
findCaseVariants(QDir(pluginsDir).filePath(canonicalName));
+ if (variants.isEmpty()) {
+ MOBase::log::debug("syncPluginsBack: no {} variant found in '{}'",
+ canonicalName, pluginsDir);
+ return true;
+ }
+
QString newest;
QDateTime newestTime;
for (const QString& v : variants) {
@@ -560,39 +573,40 @@ bool WinePrefix::syncPluginsBack(const QString& profilePluginsPath,
newest = v;
}
}
- return newest;
- };
-
- bool ok = true;
- const QString newestPlugins = pickNewest("plugins.txt");
- if (!newestPlugins.isEmpty()) {
- MOBase::log::info("syncPluginsBack: '{}' <- '{}'", profilePluginsPath,
- newestPlugins);
- if (!copyFileWithParents(newestPlugins, profilePluginsPath)) {
- MOBase::log::error("syncPluginsBack: failed to copy plugins.txt back to '{}'",
- profilePluginsPath);
- ok = false;
+ MOBase::log::info("syncPluginsBack: '{}' <- '{}'", profilePath, newest);
+ if (!copyFileWithParents(newest, profilePath)) {
+ MOBase::log::error("syncPluginsBack: failed to copy {} back to '{}'",
+ canonicalName, profilePath);
+ return false;
}
- } else {
- MOBase::log::debug("syncPluginsBack: no plugins.txt variant found in '{}'",
- pluginsDir);
- }
- const QString newestLoadOrder = pickNewest("loadorder.txt");
- if (!newestLoadOrder.isEmpty()) {
- MOBase::log::info("syncPluginsBack: '{}' <- '{}'", profileLoadOrderPath,
- newestLoadOrder);
- if (!copyFileWithParents(newestLoadOrder, profileLoadOrderPath)) {
- MOBase::log::error("syncPluginsBack: failed to copy loadorder.txt back to '{}'",
- profileLoadOrderPath);
- ok = false;
+ // Mirror newest content into any stale sibling variants so the prefix
+ // stays consistent regardless of which casing the next reader opens.
+ for (const QString& sibling : variants) {
+ if (sibling == newest) {
+ continue;
+ }
+ if (!QFile::remove(sibling)) {
+ MOBase::log::warn("syncPluginsBack: failed to remove stale sibling '{}'",
+ sibling);
+ continue;
+ }
+ if (!QFile::copy(newest, sibling)) {
+ MOBase::log::warn("syncPluginsBack: failed to mirror '{}' -> '{}'",
+ newest, sibling);
+ }
}
- } else {
- MOBase::log::debug("syncPluginsBack: no loadorder.txt variant found in '{}'",
- pluginsDir);
- }
+ return true;
+ };
+ bool ok = true;
+ if (!syncOne("plugins.txt", profilePluginsPath)) {
+ ok = false;
+ }
+ if (!syncOne("loadorder.txt", profileLoadOrderPath)) {
+ ok = false;
+ }
return ok;
}