aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xdocker/build-inner.sh24
-rw-r--r--src/src/fuseconnector.cpp22
-rw-r--r--src/src/vfs/trackedwrites.cpp54
-rw-r--r--src/src/vfs/trackedwrites.h11
4 files changed, 93 insertions, 18 deletions
diff --git a/docker/build-inner.sh b/docker/build-inner.sh
index ca6bac7..3ced87f 100755
--- a/docker/build-inner.sh
+++ b/docker/build-inner.sh
@@ -330,16 +330,22 @@ export FLUORINE_ORIG_QT_PLUGIN_PATH="${QT_PLUGIN_PATH:-}"
FLUORINE_DATA="${HOME}/.local/share/fluorine"
BIN_DST="${FLUORINE_DATA}/bin"
-# Use the main binary's mtime as a version fingerprint.
-CURRENT_VER="$(stat -c '%i:%Y' "${HERE}/ModOrganizer-core" 2>/dev/null || echo "unknown")"
-MARKER="${BIN_DST}/.version"
+# Guard: if we ARE already running from the installed location, skip the sync.
+# Without this, running the binary directly from BIN_DST would rm -rf itself.
+HERE_REAL="$(readlink -f "${HERE}")"
+DST_REAL="$(readlink -f "${BIN_DST}" 2>/dev/null || echo "")"
+if [ "${HERE_REAL}" != "${DST_REAL}" ]; then
+ # Use the main binary's mtime as a version fingerprint.
+ CURRENT_VER="$(stat -c '%i:%Y' "${HERE}/ModOrganizer-core" 2>/dev/null || echo "unknown")"
+ MARKER="${BIN_DST}/.version"
-if [ ! -f "${MARKER}" ] || [ "$(cat "${MARKER}" 2>/dev/null)" != "${CURRENT_VER}" ]; then
- echo "Syncing Fluorine to ${BIN_DST}..." >&2
- rm -rf "${BIN_DST}"
- mkdir -p "${BIN_DST}"
- (cd "${HERE}" && tar --exclude-vcs -cf - .) | (cd "${BIN_DST}" && tar -xf -)
- echo "${CURRENT_VER}" > "${MARKER}"
+ if [ ! -f "${MARKER}" ] || [ "$(cat "${MARKER}" 2>/dev/null)" != "${CURRENT_VER}" ]; then
+ echo "Syncing Fluorine to ${BIN_DST}..." >&2
+ rm -rf "${BIN_DST}"
+ mkdir -p "${BIN_DST}"
+ (cd "${HERE}" && tar --exclude-vcs -cf - .) | (cd "${BIN_DST}" && tar -xf -)
+ echo "${CURRENT_VER}" > "${MARKER}"
+ fi
fi
# Run from the synced location.
diff --git a/src/src/fuseconnector.cpp b/src/src/fuseconnector.cpp
index 1581423..c1aa38a 100644
--- a/src/src/fuseconnector.cpp
+++ b/src/src/fuseconnector.cpp
@@ -283,10 +283,13 @@ bool FuseConnector::mount(
m_trackedWrites->load(m_trackingFilePath);
if (existed) {
m_trackedWrites->detectManualMoves(m_overwriteDir, mods);
- } else {
- // First run: scan overwrite for files that also exist in a mod
- m_trackedWrites->initialScan(m_overwriteDir, mods);
}
+ // NOTE: initialScan() is no longer called on first run. Its heuristic
+ // ("file exists in both overwrite and a mod → track it") produces false
+ // positives: game-generated overwrite files that happen to share a name
+ // with a mod file get incorrectly tracked. Tracking now only happens
+ // through explicit user actions (UI move/sync/drag-drop) or the
+ // snapshot-based detectManualMoves().
m_trackedWrites->save(m_trackingFilePath);
} else {
std::fprintf(stderr, "[VFS] WARNING: tracking file path is empty!\n");
@@ -394,11 +397,16 @@ void FuseConnector::unmount()
static_cast<long long>(ms));
}
- // After flush: scan overwrite for files that also exist in mods (bootstraps
- // tracking on first run), snapshot overwrite contents for next session's
- // manual-move detection, then save tracking data.
+ // Snapshot overwrite contents for next session's manual-move detection,
+ // then save tracking data.
+ //
+ // NOTE: initialScan() is intentionally NOT called here. It was previously
+ // run at every unmount, which caused false-positive tracking: any file that
+ // exists in both overwrite and a mod got auto-tracked, even if the overwrite
+ // copy was a game-generated modification (not a user move). Tracking should
+ // only happen through explicit user actions (UI move/sync) or the
+ // snapshot-based detectManualMoves() at mount time.
if (m_trackedWrites && !m_trackingFilePath.empty()) {
- m_trackedWrites->initialScan(m_overwriteDir, m_lastMods);
m_trackedWrites->snapshotOverwrite(m_overwriteDir);
m_trackedWrites->save(m_trackingFilePath);
}
diff --git a/src/src/vfs/trackedwrites.cpp b/src/src/vfs/trackedwrites.cpp
index 4392db3..62bb88b 100644
--- a/src/src/vfs/trackedwrites.cpp
+++ b/src/src/vfs/trackedwrites.cpp
@@ -84,6 +84,9 @@ void TrackedWrites::load(const std::string& path)
const std::string data = ss.str();
f.close();
+ // Base directory for resolving relative mod paths.
+ const std::string baseDir = fs::path(path).parent_path().string();
+
std::lock_guard lock(m_mutex);
m_tracked.clear();
m_overwriteSnapshot.clear();
@@ -136,7 +139,17 @@ void TrackedWrites::load(const std::string& path)
++pos;
std::string modPath = parseJsonString(data, pos);
- m_tracked[toLower(relPath)] = modPath;
+ // Resolve mod path: if it's relative, resolve against base dir.
+ // If it's absolute (legacy format), use as-is for backwards compat.
+ std::error_code ec;
+ std::string resolved;
+ if (!modPath.empty() && modPath[0] != '/') {
+ resolved = fs::weakly_canonical(fs::path(baseDir) / modPath, ec).string();
+ } else {
+ resolved = modPath;
+ }
+
+ m_tracked[toLower(relPath)] = resolved;
skipWhitespace(data, pos);
if (pos < data.size() && data[pos] == ',')
@@ -171,6 +184,9 @@ void TrackedWrites::load(const std::string& path)
}
std::fprintf(stderr, "[VFS] loaded %zu tracked write mappings\n", m_tracked.size());
+
+ // Prune entries whose mod folder no longer exists (e.g. user deleted the mod).
+ pruneStaleUnlocked();
}
void TrackedWrites::save(const std::string& path) const
@@ -187,12 +203,22 @@ void TrackedWrites::save(const std::string& path) const
return;
}
+ // Base directory for making mod paths relative (portable).
+ const fs::path baseDir = fs::path(path).parent_path();
+
f << "{\n \"tracked\": {";
bool first = true;
for (const auto& [relPath, modPath] : m_tracked) {
if (!first)
f << ",";
- f << "\n \"" << jsonEscape(relPath) << "\": \"" << jsonEscape(modPath) << "\"";
+ // Convert absolute mod path to relative for portability.
+ std::string storedPath = modPath;
+ if (!modPath.empty() && modPath[0] == '/') {
+ auto rel = fs::relative(fs::path(modPath), baseDir, ec);
+ if (!ec && !rel.empty())
+ storedPath = rel.string();
+ }
+ f << "\n \"" << jsonEscape(relPath) << "\": \"" << jsonEscape(storedPath) << "\"";
first = false;
}
f << "\n },\n \"overwrite_snapshot\": [";
@@ -209,6 +235,30 @@ void TrackedWrites::save(const std::string& path) const
std::fprintf(stderr, "[VFS] saved %zu tracked write mappings\n", m_tracked.size());
}
+void TrackedWrites::pruneStale()
+{
+ std::lock_guard lock(m_mutex);
+ pruneStaleUnlocked();
+}
+
+void TrackedWrites::pruneStaleUnlocked()
+{
+ std::error_code ec;
+ int pruned = 0;
+ for (auto it = m_tracked.begin(); it != m_tracked.end(); ) {
+ if (!fs::is_directory(it->second, ec)) {
+ std::fprintf(stderr, "[VFS] pruning stale tracking: %s -> %s (mod folder gone)\n",
+ it->first.c_str(), it->second.c_str());
+ it = m_tracked.erase(it);
+ ++pruned;
+ } else {
+ ++it;
+ }
+ }
+ if (pruned > 0)
+ std::fprintf(stderr, "[VFS] pruned %d stale tracked write entries\n", pruned);
+}
+
void TrackedWrites::track(const std::string& relative_path,
const std::string& mod_folder_path)
{
diff --git a/src/src/vfs/trackedwrites.h b/src/src/vfs/trackedwrites.h
index c9218ae..a8ebe08 100644
--- a/src/src/vfs/trackedwrites.h
+++ b/src/src/vfs/trackedwrites.h
@@ -24,11 +24,19 @@ public:
TrackedWrites() = default;
// Load tracking data from a JSON file.
+ // Mod paths are stored relative to the JSON file's parent directory for
+ // portability. They are resolved to absolute paths on load.
void load(const std::string& path);
// Save tracking data to a JSON file.
+ // Absolute mod paths are converted to relative (relative to the JSON file's
+ // parent directory) before writing.
void save(const std::string& path) const;
+ // Remove entries whose mod folder no longer exists on disk.
+ // Called automatically after load(), but can also be called explicitly.
+ void pruneStale();
+
// Record that a file at relative_path now lives in mod_folder_path.
// mod_folder_path is the absolute path to the mod's root directory.
void track(const std::string& relative_path, const std::string& mod_folder_path);
@@ -62,6 +70,9 @@ public:
std::unordered_map<std::string, std::string> allMappings() const;
private:
+ // Prune without locking (caller must hold m_mutex).
+ void pruneStaleUnlocked();
+
// relative_path (lowercase) -> absolute mod folder path
mutable std::mutex m_mutex;
std::unordered_map<std::string, std::string> m_tracked;