diff options
| -rwxr-xr-x | docker/build-inner.sh | 24 | ||||
| -rw-r--r-- | src/src/fuseconnector.cpp | 22 | ||||
| -rw-r--r-- | src/src/vfs/trackedwrites.cpp | 54 | ||||
| -rw-r--r-- | src/src/vfs/trackedwrites.h | 11 |
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; |
