From 37e13e0f5aa8a400ca3bee77d37cbe73b9919355 Mon Sep 17 00:00:00 2001 From: SulfurNitride Date: Mon, 16 Feb 2026 21:26:51 -0600 Subject: Fix FUSE VFS reporting wrong timestamps on save files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three issues caused all save files to show the same date (the session start time), breaking Starfield's "Continue" mechanism: 1. setattr ignored FUSE_SET_ATTR_MTIME — Wine's SetFileTime() calls (utimensat) were silently dropped, so games couldn't set timestamps 2. TTL_SECONDS was 365 days — kernel cached stale attrs and never re-queried FUSE even after writes updated the real file 3. mo2_rename replaced mtime with now() instead of preserving it Fixes #16 Co-Authored-By: Claude Opus 4.6 --- src/src/vfs/mo2filesystem.cpp | 43 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/src/vfs/mo2filesystem.cpp b/src/src/vfs/mo2filesystem.cpp index 7842c1a..7b440f9 100644 --- a/src/src/vfs/mo2filesystem.cpp +++ b/src/src/vfs/mo2filesystem.cpp @@ -1,6 +1,7 @@ #include "mo2filesystem.h" #include +#include #include #include @@ -13,7 +14,7 @@ namespace { namespace fs = std::filesystem; -constexpr double TTL_SECONDS = 60.0 * 60.0 * 24.0 * 365.0; +constexpr double TTL_SECONDS = 1.0; struct NodeSnapshot { @@ -595,7 +596,7 @@ void mo2_rename(fuse_req_t req, fuse_ino_t parent, const char* name, const std::string over = ctx->overwrite->overwritePath(newRelative); const std::string real = fs::exists(staged) ? staged : over; ctx->tree->root.insertFile(splitPath(newRelative), real, oldSnap.size, - std::chrono::system_clock::now(), "Staging"); + oldSnap.mtime, "Staging"); } } @@ -689,6 +690,44 @@ void mo2_setattr(fuse_req_t req, fuse_ino_t ino, struct stat* attr, int to_set, updateFileNode(ctx, path, target, "Staging"); } + // Handle explicit timestamp changes (utimensat / Wine SetFileTime) + if ((to_set & (FUSE_SET_ATTR_MTIME | FUSE_SET_ATTR_MTIME_NOW | + FUSE_SET_ATTR_ATIME | FUSE_SET_ATTR_ATIME_NOW)) != 0 && + attr != nullptr) { + const auto snap = snapshotForPath(ctx, path); + if (snap.found && !snap.is_directory) { + // Apply the timestamp to the real file on disk + struct timespec times[2]; + // atime + if (to_set & FUSE_SET_ATTR_ATIME_NOW) { + times[0].tv_sec = 0; + times[0].tv_nsec = UTIME_NOW; + } else if (to_set & FUSE_SET_ATTR_ATIME) { + times[0] = attr->st_atim; + } else { + times[0].tv_sec = 0; + times[0].tv_nsec = UTIME_OMIT; + } + // mtime + if (to_set & FUSE_SET_ATTR_MTIME_NOW) { + times[1].tv_sec = 0; + times[1].tv_nsec = UTIME_NOW; + } else if (to_set & FUSE_SET_ATTR_MTIME) { + times[1] = attr->st_mtim; + } else { + times[1].tv_sec = 0; + times[1].tv_nsec = UTIME_OMIT; + } + + utimensat(AT_FDCWD, snap.real_path.c_str(), times, 0); + + // Update the VFS tree node with the new mtime + if (to_set & (FUSE_SET_ATTR_MTIME | FUSE_SET_ATTR_MTIME_NOW)) { + updateFileNode(ctx, path, snap.real_path, "Staging"); + } + } + } + const auto snap = snapshotForPath(ctx, path); if (!snap.found) { fuse_reply_err(req, ENOENT); -- cgit v1.3.1