aboutsummaryrefslogtreecommitdiff
path: root/src
Commit message (Collapse)AuthorAgeFilesLines
...
* prefix: describe installer exit codesSulfurNitride2026-05-232-7/+30
|
* prefix: improve setup progress and failure logsSulfurNitride2026-05-234-42/+217
|
* prefix: align dependency setup and VFS fixesSulfurNitride2026-05-239-76/+778
|
* vfs: preserve canonical casing for write destinationsSulfurNitride2026-05-191-3/+6
|
* mo2: catch up to upstream 2.5.3 Betas 3-11SulfurNitride2026-05-1622-1673/+1136
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Multi-area sync against the upstream MO2 2.5.3 beta line. Roughly three logical sections share the diff: Catch-up (Betas 3-11): - nxmaccessmanager: drop "supporter" from validRoles so non-premium supporters get the right download path (Beta 10) - mainwindow: disable tutorial triggers at the trigger sites; Qt 6.11 lockup on child windows (Beta 4) - basic_games: add Kingdom Come Deliverance 2, Slay the Spire 2 (Betas 11, 6) - uibase: add NXM collections parsing fields/accessors + (?:nxm|modl) scheme support in NXMUrl (Beta 9) - Starfield: blueprintPrefix() on IPluginGame; plugins.txt write suppression for blueprint plugins; hasInvalidBlueprint / hasUnpairedBlueprint diagnoses; Title-keyed content catalog consolidation (Betas 3-5) - downloadmanager + systemtraymanager + organizercore + iuserinterface + settings: add Beta 11 "show notifications when downloads complete or fail" toggle and the showNotification plumbing - nxmaccessmanager: restore upstream re-entry guard for OAuth refresh and the styled OAuth response page (Beta 11) - organizer_en.ts and game-bethesda *_en.ts: pull upstream HEAD strings Workarounds tab removed: - Delete settingsdialogworkarounds.{h,cpp}; drop the tab in settingsdialog.ui; move "Enable archives parsing" into General - Hardcode GameSettings::forceEnableCoreFiles() to true so the primary-master toggle-off bug (DLCs unchecked on tab refresh) can't recur; setter becomes a no-op - No-op stubs for offlineMode, useProxy, useCustomBrowser, Steam appID/credentials, executablesBlacklist, skipFileSuffixes, skipDirectories — UI gone, accessors keep call sites green - settingsdialognexus drops the custom-browser wiring; spawn drops the "Change the blacklist" Retry path Beta 9 download manager port: - fileID-first match in nxmFilesAvailable and nxmFileInfoFromMd5Available; filename remains the fallback - NexusInterface::isActiveFileStatus() helper; rewrite of nxmUpdatesAvailable to use pickNewestVersion / findUpdateChainSuccessors / resolveInstalledFileId. ARCHIVED_HIDDEN now correctly treated as inactive - std::optional<unsigned int> reservedID on DownloadInfo factories and on the three addDownload overloads; startDownloadURLs and startDownloadURLWithMeta return the canonical DownloadID instead of m_ActiveDownloads.size()-1 (which was the index, not an ID) - QHash<unsigned int, DownloadInfo*> m_ByID maintained alongside m_ActiveDownloads at every mutation site; downloadInfoByID is now O(1) via m_ByID.value(id, nullptr) LOOT remains intentionally stripped (lootcli build cruft cleaned up separately). BSA wide-path skipped — Linux UTF-8 paths already work. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ui: inline trash icon on backup-restore dialogSulfurNitride2026-05-163-5/+154
| | | | | | | | | | | | | | | | | | | | | | User feedback: the only way to clean up old backups was to hand-edit the profile directory, since the "Choose backup to restore" dialog exposed select-or-cancel and no delete affordance. Add an addChoice overload on SelectionDialog that takes a per-row delete callback. Each row is rendered as a horizontal layout — QCommandLinkButton (stretched) plus a small auto-raised QToolButton with a trash icon (user-trash → edit-delete → SP_TrashIcon fallback chain). Clicking the trash prompts for confirmation, invokes the callback, and hides the row on success; the QCommandLink remains live for restore. numChoices() now counts visible rows under scrollAreaWidgetContents so the "no backups" check stays correct as rows disappear. MainWindow::queryRestore wires the callback to delete the matching Plugins.txt.<ts>, loadorder.txt.<ts>, and lockedorder.txt.<ts> triplet via QFile::remove. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* plugins: surface plugin name and Python exception on init failureSulfurNitride2026-05-161-1/+8
| | | | | | | | | | | | | | | | | | | | | | | "plugin failed to initialize" with no name and no exception text is the canonical first-line indicator for a Python plugin that didn't register — but it tells the user nothing about *which* plugin or *why*. Encountered while diagnosing a flaky first-launch failure where a Python plugin's init() silently returned False on cold boot and succeeded on the next run, plus a Ready or Not instance that wouldn't open. Wrap the IPlugin::init trampoline in pyplugins.h manually instead of relying on PYBIND11_OVERRIDE_PURE so we can: - log the plugin's name and the surrounding hint when init() returns False (typically a plugin swallowing an exception in its own try/except); - catch pybind11::error_already_set and log the Python exception text + traceback before returning False to the C++ caller. plugincontainer.cpp's "plugin failed to initialize" line now also prints the plugin name so the proxied-candidate context is clear. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ui: recover from stale FUSE mounts without bothering the userSulfurNitride2026-05-162-13/+57
| | | | | | | | | | | | | | | | | | | | | | | | | Two complementary changes against ENOTCONN — "Transport endpoint is not connected" — from a wedged FUSE mount left by a previous crashed or unclean session. MOApplication::notify already caught filesystem_error and called FuseConnector::tryCleanupStaleMount on the affected path, but it still went on to show the user a hard error dialog about the exception. If we've already cleared the wedged mount the workflow that triggered the iterator (refresh, restore backup, etc.) just needs to be retried — the dialog is noise on a state we just recovered from. Probe stat() after cleanup; if the path is no longer wedged, log + swallow. OrganizerCore::afterRun's post-game chmod loop used the throwing recursive_directory_iterator constructor with no error_code, so a single ENOTCONN under the game dir unwound out of afterRun and left the rest of the post-run sync (save/INI/plugin sync-back, load-order refresh, FinishedRun callback) un-run. Switch to error_code-based iteration; on ENOTCONN, call tryCleanupStaleMount on the game dir and continue cleanly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* vfs: invalidate full subtree from node_cache on mutation (fixes #210)SulfurNitride2026-05-162-43/+72
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | invalidateNodeCache's docstring promised "Clear all node_cache entries whose path starts with the given prefix" but the implementation only cleared the exact path's entry and its parent. removeFromTree on a directory destroys every descendant VfsNode via cascading unique_ptr destruction; every descendant inode still cached in node_cache became a dangling pointer. Next time Wine called getattr on a cached descendant ino, resolveByInode returned the dangling pointer and snapshotFromNode read freed memory — depending on what reused the allocation, this manifested as either std::bad_alloc (corrupted string length in real_path triggers a huge allocation request) or outright SIGSEGV. ENOMEM bubbled up to Wine's NT loader as ERROR_MOD_NOT_FOUND (0x7E), which is exactly the F4SE "couldn't load plugin (0000007E)" pattern reported in #210 after mod-state transitions. Rewrite invalidateNodeCache to actually walk node_cache and erase every entry whose path is the target or a descendant under target/. Lock order is tree_mutex (exclusive, caller) → inode_mutex (shared) → node_cache_mutex (exclusive), consistent with existing sites. Top-level mutations also invalidate root (inode 1) since its children map changed. Also enrich the FUSE wrap_* exception macro to log the inode being processed alongside the exception text. Future "[VFS] getattr(ino=N): caught exception: ..." entries point straight at the path that triggered cache/tree corruption. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ui: always lock during executable run; kill wineserver on UnlockSulfurNitride2026-05-165-60/+4
| | | | | | | | | | | | | | | | | | The "Lock GUI when running executable" toggle is removed from the Workarounds settings tab. Locking is required for the post-run refresh to know when files changed under the prefix, and the off-state was a well-documented foot-gun. The setter/getter on InterfaceSettings, the QSettings key (Settings/lock_gui), the checkbox widget, the tabstop entry, and the two no-lock branches in ProcessRunner::postRun all go. When the user clicks Unlock in the lock dialog, also SIGTERM the tracked game pid and hard-kill the prefix's wineserver. The Cancelled branch (Cancel in PreventExit mode) already did this; the ForceUnlocked branch silently returned and left wineserver running, so Proton's session manager held the prefix open for tens of seconds and the next launch inherited a dirty state. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* themes: shim asset case both ways for QSS url() refsSulfurNitride2026-05-161-18/+31
| | | | | | | | | | | | | | | Stylesheet themes ported from Windows can reference assets with either case convention - some QSS files use lowercase url() refs against MixedCase.svg files on disk, others do the reverse. The original shim only covered the first direction. Add the second so themes whose disk assets are lowercase but whose QSS uses TitleCase also resolve on case-sensitive filesystems. Rename createLowercaseStylesheetShims -> createStylesheetCaseShims and extract the symlink-creation into a tryShim lambda that's called twice per file (lowercase + TitleCase target names). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* vfs: drop experimental PE-side bridge / shim DLL stackSulfurNitride2026-05-1622-5995/+12
| | | | | | | | | | | | | | | Removes the FUSE-bypass fast-path: fluorine_vfs.dll + fluorine_vfs_hid.dll (mingw-cross PE-side AppInit_DLLs injector with MinHook), the LD_PRELOAD helper libfluorine_vfs_preload.so, the bridge-index export from FuseConnector, the prewarm + Wine-loader-trace plumbing in ProtonLauncher, AppInit_DLLs prefix-init staging, the per-executable UseVfsBridge flag plus its checkbox/handlers in editexecutablesdialog, and the third_party/minhook vendored tree. Core FUSE VFS (mo2filesystem, vfstree, inodetable, overwritemanager, scancache, trackedwrites) is untouched - this only removes the sidecar. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ui: take down full process tree and unmount VFS on force-unlockSulfurNitride2026-05-161-16/+85
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | The previous force-unlock path SIGTERMed only displayPid (one tracked .exe) and called killWineserverForPrefix(winePrefix), but winePrefix was read from the root pid — a Proton wrapper that frequently lacks WINEPREFIX in its environ even though the game pid underneath has it. When that lookup returned empty, killWineserverForPrefix bailed early, wineserver stayed alive, and the rest of the Wine process tree survived. Skyrim via SKSE was the canonical failure mode: skse_loader exits early, the tracked pid becomes SkyrimSE.exe, SIGTERM hits that one pid, audio/physics workers keep running. ForceUnlocked also returned without calling afterRun(), because shouldRefresh(ForceUnlocked) returned false to "avoid racing with file updates." That gated the FUSE unmount along with the directory refresh, so the VFS mount under the game directory leaked across launches. Add killProcessTree(pid_t root) using the existing children/descendant helpers — SIGTERM the whole tree, wait briefly, SIGKILL survivors. Merge ForceUnlocked and Cancelled into a shared branch that: - resolves an effective WINEPREFIX by falling through displayPid, lastTrackedPid, and root pid until one carries the env var; - kills the descendant tree of the root pid (covers launcher .exe grandchildren that would otherwise survive); - hard-kills wineserver for the resolved prefix; - sets exitCode = 1 so afterRun()'s plugin-sync gate refuses to trust possibly-half-written Plugins.txt. Flip shouldRefresh(ForceUnlocked) to true so afterRun() runs and the FUSE VFS unmounts. The "racing with writes" concern is moot now that every Wine process is dead before we return from the branch. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* nxm: stop launcher .desktop from claiming nxm:// + clear stale portal pickSulfurNitride2026-05-132-1/+28
| | | | | | | | | | | | | | | | | | | | | | The Fluorine Manager launcher .desktop declared MimeType=x-scheme-handler/nxm;x-scheme-handler/modl;, advertising itself as a handler alongside the real mo2-nxm-handler.desktop. The launcher's Exec line has no %u, so when xdg-desktop-portal launched it the URL was dropped and MO2 came up with no args — which then hit the "instance already running" path against the live primary. Drop the bad MimeType from both the tarball template (data/icons/) and the .bin installer's inline desktop entry in docker/build-inner.sh. That alone fixes new installs, but xdg-desktop-portal remembers user picks in its permission store: anyone who'd already selected Fluorine Manager in the chooser dialog had it stuck as their always-use app (count=3/3 = silent always-launch), and the bad pick survived removing the MimeType. On startup the nxm handler now fires a DBus DeletePermission against PermissionStore for com.fluorine.manager on both schemes, so existing users self-heal on the next launch. The call is fire-and-forget — a missing entry is the expected steady state. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* nxm: drain socket lines before emitting to dodge use-after-freeSulfurNitride2026-05-131-2/+10
| | | | | | | | | | | | | | | | | processSocketData iterated socket->canReadLine() and emit'd nxmReceived inside the loop. nxmReceived routes to OrganizerCore which can show a modal dialog (the "Wrong Game" warning fires whenever the link's game domain doesn't match the active instance). Modal dialogs spin the event loop, and the loop processes the disconnected → deleteLater queued for the same socket. The socket gets freed, then control returns to the while loop and the next canReadLine() runs on a dangling pointer. Repro hits cleanly when an nxm:// for a non-matching game arrives — the "Wrong Game" dialog appears and as soon as the user dismisses it MO2 crashes with SIGSEGV. Read into a QStringList first, then emit after the read loop ends so the socket is no longer touched once handlers run. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* multiprocess: claim primary listener when stale shm can't be reclaimedSulfurNitride2026-05-131-21/+20
| | | | | | | | | | | | | | | | | | | | | | | Walked into this when a download manager click silently popped "An instance of Mod Organizer is already running" with no MO2 actually visible. State on disk: SysV shm segment from a prior MO2 still there with nattch=0 (kernel won't reap until IPC_RMID, which only the dead creator could call) plus the orphaned /tmp/mo-<key> socket file. The stale-recovery path was attaching, finding no listener via primaryAlive(), then trying to detach+create — which kept failing because the corpse segment still occupied the key — and falling all the way through to "re-attach as secondary". Result: the new launch flagged itself ephemeral, forwardToPrimary() had nothing to send, and the user got the ghost dialog. The shm is only advisory; the unix socket listener is the real lock. If primaryAlive() said no one's listening, we should run as primary even when we can't reclaim the orphaned segment. Also re-probe on listen() failure so a real race (another process becoming primary between our probe and our listen) demotes us cleanly instead of leaving us as a broken primary. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* multiprocess: silence liveness-probe close as benignSulfurNitride2026-05-081-4/+15
| | | | | | | | | | | | | | | primaryAlive() opens a QLocalSocket then disconnects without writing any payload to verify a primary is listening. The primary's receiveMessage() saw the empty PeerClosedError connection and logged "failed to receive data from secondary process" plus a popup, even though the real NXM/shortcut message arrived on the next connection and was processed correctly. Treat zero-byte PeerClosedError/UnknownSocketError closes as the expected probe pattern (debug log, no error). Other errors still report loudly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* vfs: per-executable opt-in toggle for PE bridgeSulfurNitride2026-05-087-17/+65
| | | | | | | | | | | | | | | | | | | | | | | Adds Executable::UseVfsBridge flag (off by default) and a "Use VFS bridge (experimental)" checkbox in the executables editor next to "Open in terminal" / "Use Proton". The flag plumbs through to SpawnParameters::useVfsBridge and gates the FLUORINE_VFS_INDEX/DATA_DIR/ MOUNT env vars in spawn(). Without those env vars, the staged fluorine_vfs.dll still AppInit-loads into every PE process in the prefix, but its hook_worker bails at load_index() and installs zero hooks — equivalent to the bridge being disabled. No separate gating is needed in protonlauncher.cpp because its hid.dll-proxy staging and prewarm are already conditioned on vfsBridgeIndexPresent(envVars). Existing instances load with useVfsBridge=false (key absent from ModOrganizer.ini; the loader treats absence as false). New executables created via the plugin path also default off. Users who want the perf win for a particular game tick the box; everyone else launches via the correct, slower Wine + FUSE path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* vfs: scope hook to mount, fix negcache to handle backing entriesSulfurNitride2026-05-081-17/+239
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Three bugs in the PE-side bridge collided to crash NSPBR ~54s after launch with an AV in NPCWaterAIFix.dll. Root cause: the bridge said STATUS_OBJECT_NAME_NOT_FOUND for files that exist on disk but are not in the static index, masquerading as ground truth. 1. load_index() filtered every entry whose real_path didn't start with '/'. That dropped every is_backing=true entry — for NSPBR, the entire vanilla Skyrim base layer (Skyrim.esm, all base BSAs, Video/BGS_Logo.bik). Now accept both forms; backing entries get real_nt=NULL and exist only for query / negcache logic. 2. The "open negcache" comment claimed the index was authoritative for the FUSE mount tree. With (1), it is not. Comment rewritten; the comment about attr_cache_invalidate (which never existed) is also corrected to point at attr_cache_mark_exists. 3. Hook_NtCreateFile and Hook_NtOpenFile returned ENOENT on `!attr_cache_lookup() || !cexists` — the "no entry at all" case got the same treatment as a confirmed-negative cache hit. Plugin code that calls CreateFile without a preceding GetFileAttributes would never reach Real_NtCreateFile. Now mirrors what try_kernel32_attr_hit already did: only ENOENT on confirmed negatives; pure cache miss falls through to Real_, which serves correctly via Wine + FUSE. Also includes the previously-uncommitted mount-scope fast gate so out-of-mount paths (system DLLs, registry-backed objects, unrelated drives) bypass the hook entirely with a single prefix compare on the input string — no normalization, no cache lookup, no chance of a wrong answer. Verified by a self-contained 44-assertion C reproduction at /home/luke/builds/fluorine-vfs-tests/test_negcache_logic.c (six scenarios: backing-filter exclusion, first-open without pre-query, pre-query masking, SKSE plugin probes, post-fix Skyrim recovery, nt_path_to_key edge cases). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* profile: guard plugins.txt sync against crash-time rewritesSulfurNitride2026-05-082-1/+65
| | | | | | | | | | | | | | | | | | | | | | | | | Bethesda's engine rewrites Plugins.txt as part of its shutdown sequence. On a clean exit it preserves the active set; on a crash it can serialize a partially cleared set — every plugin name still listed, but most without their leading '*'. The previous syncPluginsBack code copied that file back to the profile faithfully, after which refreshESPList + savePluginList re-derived state and persisted the broken active list. NSPBR observed this as 1170 active plugins collapsing to 54. Two independent guards: - afterRun() now skips the post-launch sync when exitCode != 0. Non-zero is a strong "do not trust the prefix file" signal. - syncPluginsBack() compares starred-line counts in candidate vs profile. Refuses the copy when the candidate would drop the active count by more than 30% relative AND more than 10 plugins absolute. Belt and suspenders for cases where the engine catches its own crash and exits 0 anyway. Both guards apply regardless of the VFS bridge state and protect every launch on every modlist. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* multiprocess: recover from stale SysV shm + unix socket after crashSulfurNitride2026-05-081-3/+58
| | | | | | | | | | | | | | | | | | | Linux QSharedMemory uses SysV segments that survive process crashes. On the next launch, m_SharedMem.create() fails with AlreadyExists and the constructor would either treat the corpse as a live primary (blocking the user behind a ghost) or throw. The unix socket file is similarly left behind and would make QLocalServer::listen() fail with AddressInUseError if reclaim succeeded. Now: probe the primary's listener via QLocalSocket::connectToServer before believing the segment is alive. If the probe fails, detach and re-create. If create then fails (another attached client keeps refcount > 0), re-attach so the constructor returns in a usable secondary state instead of silently no-op'ing every later sendMessage. Also call QLocalServer::removeServer() before listen() to clear the orphaned socket file when we did successfully reclaim. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Add PE-side VFS bridge accelerationSulfurNitride2026-05-0818-14/+5733
|
* vfs: persistent scan cache to skip mod walk on warm bootSulfurNitride2026-05-037-7/+499
| | | | | | | | | | | | | | | | Saves the merged VfsTree to ~/.local/share/fluorine/vfs_cache/ keyed by data_dir + mods + overwrite. On a hit, mounting reads the tree from a single binary file instead of re-walking every mod, which dominates mount time on heavy modlists. Validation: modlist.txt mtime+size, data/overwrite dir mtime, mod-dir mtimes in priority order. Any drift invalidates. Session-scoped state (extra-file injection, plugin timestamp stamping) is re-applied after load and never cached. rebuild() also refreshes the cache so the next cold mount hits. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix portable rename + route external links through xdg-open scrubberSulfurNitride2026-05-032-3/+35
| | | | | | | | | | | | | | | Issue #70: rename button was disabled for portable instances; the upstream restriction assumed portable = MO2-binary dir, which doesn't apply here (binary lives in ~/.local/share/fluorine/bin, instance dir is separate). Allow rename when not active, and update the portable registry so the renamed dir keeps showing up. Issue #67: QLabel::setOpenExternalLinks(true) and friends route through QDesktopServices::openUrl, which forks xdg-open with our bundled Qt env inherited — silently fails. Install a global url handler that defers to shell::Open, which already scrubs LD_LIBRARY_PATH/QT_PLUGIN_PATH. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* mainwindow: show semver in beta titlebar tooSulfurNitride2026-05-021-7/+9
| | | | | | | | | Beta titlebar was "Fluorine Manager beta @ <hash>" — the version line (0.1.x vs 0.2.x) was invisible. Switch to "<semver>-beta @ <hash>" so the running build's major/minor is obvious without opening the About dialog. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* steamdetection: stop blacklisting Proton 11SulfurNitride2026-05-021-10/+4
| | | | | | | | | The wineboot -u deadlock workaround (waitforexitandrun + ntsync env nudge) already lives in prefixsetuprunner, so the catch-all Proton 11 skip in findSteamProtons is no longer needed. Users on stock Proton 11 are now selectable alongside Proton 10 / Experimental / GE-Proton. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* updater: SLR auto-update + wire fluorine updater badge, bump 0.2.0SulfurNitride2026-05-026-35/+121
| | | | | | | | | | | | | | | | | | | | - OrganizerCore: add fluorineUpdater() getter + checkForSlrUpdates() fired from startup checkForUpdates(); detached thread runs downloadSlr when SLR already installed so steamrt4 BUILD_ID drift is picked up without a launch-time stall. - MainWindow: connect FluorineUpdater::updateAvailable to existing updateAvailable() slot so the statusbar badge + actionUpdate light up; on_actionUpdate_triggered routes to Settings -> Updates when a Fluorine update is pending (the MO2 self-updater path is no-op'd). - MainWindow launch path: drop !isSlrInstalled() short-circuit; fresh installs still get the progress dialog, up-to-date checks rely on the startup background pass. - SettingsDialog: selectTabByLabel() so MainWindow can open Updates tab. - Launcher: rm -rf update-staging/ once a new bundle has synced into bin/, so the in-app updater doesn't accumulate stale extracts. - CMakeLists.txt: 0.1.4 -> 0.2.0. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* nxmaccessmanager: fix optional<NexusOAuthTokens> deref in legacy-key pathSulfurNitride2026-05-021-5/+10
| | | | | | | | | | | | | m_Tokens on NXMAccessManager is std::optional<NexusOAuthTokens> (the NexusOAuthTokens member belongs to ValidationAttempt). The new legacy API key migration code accessed .accessToken/.apiKey directly on the optional, breaking the build. Use the optional's check + arrow access throughout, and re-check it inside the network reply continuation since the user could clear credentials before the request resolves. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* nxmaccessmanager: derive legacy API key from OAuth sessionSulfurNitride2026-05-022-0/+71
| | | | | | | | | | | | | | | | | | | | Some plugins (Plugin Browser for MO2 and a number of older Python plugins) read only the legacy `apiKey` field. The OAuth login flow populates `accessToken` but never the legacy key, so those plugins fail with "User API Key is missing" even after a successful sign-in. Nexus' v1 `/users/validate.json` endpoint accepts the OAuth bearer token and returns the user's personal API key in the `key` field. We hit it once after `notifyTokens()` (fresh login) and once via the constructor queued call (already-logged-in users on first launch with this build), then persist the result through `GlobalSettings::setNexusApiKey()`. The fetch is a no-op when an apiKey is already present (manual entry or prior session) so it never overwrites a user-set value, and silently fails on non-200 / missing field so we don't break the OAuth-only path when Nexus eventually deprecates personal API keys. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* sanitychecks: probe libfuse3 via dlopen, accept .so.4SulfurNitride2026-05-021-13/+36
| | | | | | | | | | | | | Fedora 44 and Arch ship libfuse3.so.4; the static path list only checked .so.3 / unversioned .so, so the sanity check warned "libfuse3 not found" on systems where FUSE actually works. Use dlopen() with RTLD_NOLOAD then RTLD_LAZY against libfuse3.so.{4,3,} so the dynamic loader resolves through /etc/ld.so.cache and picks up any SOVERSION the distro ships. Fallback path list extended with .so.4 for sandboxes that block dlopen. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* meta.ini: revert to upstream CamelCase, canonicalize on readSulfurNitride2026-05-016-134/+173
| | | | | | | | | | | | | | | | | | | | Restore upstream MO2's exact key cases (gameName, installationFile, nexusDescription, etc.) in readMeta/saveMeta/doInstall/createMod — matches mod-shipped meta.ini cases, so on Linux Qt6's case-sensitive QSettings IniFormat, setValue updates in place without producing a duplicate key. normalizeMetaIniCase() repurposed: instead of folding everything to lowercase (which mismatched the upstream-style setValue calls and caused the very dupes it was trying to fix), it now canonicalizes known per-mod meta.ini keys to their upstream CamelCase, deduping case-insensitive collisions per section. Cleans up dirty files written during the 5d1fb29 -> 1f19892 window so they collapse to a single canonical key per setting on first read. Tests updated to assert canonical-CamelCase output. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* meta.ini: lowercase code keys to match normalize, kill dup writesSulfurNitride2026-05-013-34/+34
| | | | | | | | | | | | | | | | | | | normalizeMetaIniCase folds keys to lowercase, but readMeta/saveMeta and the install paths still used CamelCase ("installationFile", "gameName", "nexusDescription", etc.). Qt6 IniFormat is case-sensitive on Linux, so setValue under CamelCase wrote a NEW key alongside the lowercase entry left by normalize, and value() reads with CamelCase missed the lowercase key and fell back to defaults — saveMeta then persisted those defaults under CamelCase. Net effect: every save grew meta.ini by ~12 duplicate lines (e.g. installationFile= + installationfile=<real value>; gameName=SkyrimSE + gamename=Skyrim). Lowercased all per-mod meta.ini key names in modinforegular.cpp, installationmanager.cpp, and organizercore.cpp. Section/group names (installedFiles, Plugins, INI Tweaks) stay CamelCase because normalize preserves section headers verbatim. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Expose root storage paths to SLRSulfurNitride2026-05-011-1/+245
|
* Remove AppImage build target and runtime referencesSulfurNitride2026-05-017-67/+34
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | We ship via portable tarball directory + .bin self-extracting installer only; the AppImage path was unmaintained and pulling in linuxdeploy tooling for a format we no longer publish. Build: - build.sh: drop appimage/all-includes-appimage modes (now: tarball, installer, all=both, shell). Output listing tracks the directory. - docker/Dockerfile: drop BUILD_APPIMAGE arg and the linuxdeploy download/extract block. - docker/build-inner.sh: drop build_appimage() (~120 lines), AppImage arm of the BUILD_MODE switch, *.AppImage summary listing. - docker/AppRun.sh: deleted. - .gitignore: drop *.AppImage / squashfs-root / *.flatpak entries. Runtime: no longer key off APPIMAGE/APPDIR env vars or AppRun-set state. The fluorine-manager launcher script already exports FLUORINE_ORIG_*, MO2_BASE_DIR, MO2_PLUGINS_DIR, MO2_LIBS_DIR, MO2_PYTHON_DIR for the same purpose, so simplify: - envshortcut: appImageOrBinary -> launcherOrBinary; bundledFluorineIcon no longer probes APPDIR. - nxmhandler: drop APPIMAGE-based wrapper path; use applicationFilePath. - protonlauncher: rename cleanAppImageEnv -> cleanFluorineEnv, drop APPIMAGE/APPDIR/OWD/ARGV0/APPIMAGE_ORIGINAL_EXEC/DESKTOPINTEGRATION removals and .mount_Fluori pattern strip. - prefixsetuprunner: same env-cleaning trim. - utility, library.h, proxypython, mainwindow, moapplication, appconfig: comment cleanup pointing at the launcher instead of AppRun. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Fix meta.ini duplicate keys and SKSE Log Redirector mapped foldersSulfurNitride2026-05-019-7/+788
| | | | | | | | | | | | | | | | | | | | | | | | | | | | meta.ini: Qt6 QSettings IniFormat is case-sensitive on Linux. Pre-existing CamelCase keys + lowercase setValue() left both casings in the file, so mods grew duplicate entries on every install. Added MetaIniUtils:: normalizeMetaIniCase() to fold keys to lowercase and dedupe per-section (keeping non-empty values) before any QSettings open in readMeta, saveMeta, doInstall, and createMod. SKSE Log Redirector: deployExternalMappings created per-file symlinks under the redirected destination, so any new log file the game wrote post-deploy ended up as a real file outside the mapping. For isDirectory && createTarget mappings, now publish a single directory symlink dst -> src when the destination is missing, an existing symlink, or empty. Falls back to per-file symlinks when dst already has real content (preserves user data). Cleanup removes the symlink, leaving no files behind. Tests under src/tests/ (gtest, opt-in via BUILD_TESTING): - test_metainiutils: 6 tests covering no-op cases, case-only dedup, multi-line continuations, per-section scoping, and end-to-end verification that QSettings duplication is fixed. - test_external_dir_mapping: 6 tests covering directory symlink deploy, flow-through writes, idempotency, empty-dir replacement, real-file fallback, and cleanup. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Port upstream PR #2368: preview parent widget fixesSulfurNitride2026-05-013-3/+4
| | | | | | | | | | | | | | | Cherry-picked the three preview-parent fixes from #2368: parentWidget() in ConflictsTab/FileTreeTab/ImagesTab returns the tab widget itself, not the dialog — preview windows ended up parented to the wrong widget. m_parent->parentWidget() returns the actual mod info dialog so previews position and z-order correctly. Skipped the Starfield blueprint feature from the same PR — requires coordinated changes across uibase (add IPluginGame::blueprintPrefix) and modorganizer-game_bethesda (Starfield override). See project_upstream_pending_prs.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Port upstream PR #2384: --name/--modname/--version/--source on download CLISulfurNitride2026-05-014-1/+50
| | | | | | | | | | | | | | Adds metadata flags to the `download` subcommand and a corresponding DownloadManager::startDownloadURLWithMeta() entry point that populates ModRepositoryFileInfo before queueing. Skipped the upstream settings.cpp hunk that re-registers the Windows nxmhandler.exe binary for both nxm:// and modl:// — our nxmhandler_linux already registers both MIME schemes in registerHandler(). Cherry-picked from upstream/dev/modl-handler against merge-base dc420a25. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Port upstream PR #2341: -i "" launches portable instanceSulfurNitride2026-05-011-5/+19
| | | | | | | | | | Distinguish between -i (no value, prints current instance and exits) and -i "" (launches the portable instance if one exists) by switching the option parser from std::string to boost::optional<std::string>. Cherry-picked from upstream 49da80c. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Port upstream PR #2379: reset CWD to app dir on startupSulfurNitride2026-05-011-0/+7
| | | | | | | | | | | | | When MO2 is launched by the nxm handler from a browser, CWD is whatever the browser inherited (often /, $HOME, or the user's Desktop). Reset it to the application directory so any code path relying on CWD behaves the same as a normal launch. Cherry-picked from upstream f80ad04. Comment expanded for the Linux context (Qt resource lookup, relative QFile paths, QtWebEngine sandbox helper) — upstream's note only covered QtWebEngineProcess on Windows. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Port upstream PR #2374: Nexus OAuth authenticationSulfurNitride2026-05-0123-1444/+2085
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Replaces legacy API-key flow with OAuth 2.0 PKCE (public client, client_id "modorganizer2", overridable via MO2_NEXUS_CLIENT_ID env). Backwards-compatible: stored API key is used as fallback if no OAuth token is present. Fixes the symptom users hit after Nexus deprecated personal API keys for download endpoints — Settings showed "Connected." (key still validates) but downloads/CDN list returned empty. Cherry-picked from upstream/dev/oauth-graphql against merge-base 925bade3, with these adjustments for our Linux-only fork: - Dockerfile: aqtinstall +qtnetworkauth module - CMakeLists.txt: find_package + link Qt6::NetworkAuth - Dropped Windows-only hunks: dlls.manifest.qt6{,debug}, pch.h QWebSocket include - Skipped src/CMakeLists.txt and .gitignore upstream hunks (their layout differs from ours) - Skipped organizer_en.ts (regenerable via lupdate) - Skipped tutorials/tutorial_firststeps_settings.js (defer to the tutorials-disable PR port) - Kept our defensive default member initialisers in nxmaccessmanager.h (m_Reply{nullptr}, m_Result{None}, etc.) and `override` on createRequest() - Resolved trivial conflicts in modlistviewactions.cpp by taking upstream's NexusOAuthTokens API while retaining our [=, this] capture style - createinstancedialog.h, instancemanager.cpp: only meaningful PR delta is a comment text change / unrelated drift; ours retained OAuth callback uses local loopback http://127.0.0.1:28635/callback served by QOAuthHttpServerReplyHandler (no firewall punch needed). Token storage piggybacks the existing Linux QSettings INI credential backend (~/.config/ModOrganizer/credentials.ini). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Clean up empty dirs left by VFS, suppress noisy Qt debugSulfurNitride2026-04-302-14/+113
| | | | | | | | | | | | | | | Fixes #64: VFS now tracks the directories it creates outside the data dir (external mappings + RootBuilder game-root deploys) and removes them on unmount, but only if still empty. Pre-existing user dirs and the game root itself are never touched. RootBuilder manifest gains a "dirs" field so the cleanup survives a crashed session. Also drops a Path-repr qDebug in bg3_file_mapper that crashed Qt's logger on surrogate-escaped paths, and defaults QT_LOGGING_RULES to default.debug=false in the launcher and AppRun (still overridable) so the same class of plugin bug stays muted in shipped builds. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Clean up remaining Linux packaging namesSulfurNitride2026-04-291-1/+1
|
* Clean up clang-tidy diagnosticsSulfurNitride2026-04-2927-77/+100
|
* Finalize Linux build cleanupSulfurNitride2026-04-2990-53173/+53029
|
* clang-tidy: misc-const-correctness + performance-for-range-copy passSulfurNitride2026-04-2982-917/+917
| | | | | | | | | | | | | | Final auto-fix sweep that adds const to local variables and switches range-for-by-value to range-for-by-const-ref where the element type is non-trivially-copyable. The auto-fixer also emitted ~15 invalid \`for (const T& const x : ...)\` range loops where \`const-correctness\` and \`for-range-copy\` both fired on the same line. Hand-fixed via sed: \`& const \` -> \`& \`. Build verified. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* clang-tidy: third auto-fix passSulfurNitride2026-04-28137-516/+516
| | | | | | | | | | | | | | | | | | | | | | | | | | Sequentially applied a tighter set of safer checks: - modernize-return-braced-init-list - modernize-use-equals-default - modernize-use-noexcept - modernize-use-using - modernize-raw-string-literal - readability-enum-initial-value - readability-make-member-function-const - readability-convert-member-functions-to-static - readability-redundant-member-init - readability-container-contains - readability-container-size-empty One file (mainwindow.h) had to be hand-fixed: extractProgress was auto-marked static by readability-convert-member-functions-to-static, but it's bound via boost::bind(&MainWindow::extractProgress, this, ...) — making it static breaks the bind. Restored to a non-static member. Build verified. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* clang-tidy: second auto-fix pass + manual narrowing-conversion fixesSulfurNitride2026-04-2871-191/+182
| | | | | | | | | | | | | | | | | | | The first auto-fix run kept going in the background after the wait loop returned (a podman-detached quirk), so it produced another batch of fixes against ~52 more files that needed verification. Two cases the auto-fixer can't fix safely on its own: - DWORD m_exitCode{-1} -> static_cast<DWORD>(-1) on the brace init, because -1 narrows to unsigned int. - {EndorsedState::X} / {TrackedState::X} initializers had to be qualified with their parent namespace (MOBase::) because the identifier wasn't in scope at the point of in-class default-init. Build verified. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* clang-tidy: apply auto-fixes from safest checks across our authored codeSulfurNitride2026-04-28138-789/+781
| | | | | | | | | | | | | | | | | | | | | | Ran sequentially (not parallel — concurrent fixes corrupt shared headers) on src/src/ + libs/skse_log_redirector/. The check set: - modernize-use-override - modernize-use-nullptr - modernize-use-default-member-init - readability-redundant-member-init - readability-container-contains - readability-container-size-empty Net effect: ~108 files updated, 425/426 lines changed (overrides added, NULL/0 -> nullptr, member-init lists pruned where the same value is already in the in-class default). Build verified after revert/re-apply. Also disable readability-redundant-access-specifiers in .clang-tidy: the check is confused by Qt's `private slots:` specifier and strips the `private:` that follows it, breaking MOC. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Strip remaining Windows-only conditionals across src/src/SulfurNitride2026-04-2857-10125/+4200
| | | | | | | | | | | | | | | | | | | | | | | | Removes every #ifdef _WIN32 / #ifndef _WIN32 / Q_OS_WIN block from the organizer source tree. The Linux build is the only target this fork produces, so the dead Windows branches were just noise. Stubs for Win32-shaped APIs that the upstream code references (FILETIME, HANDLE, DWORD, etc.) live in shared/windows_compat.h so the surrounding source keeps compiling without rewriting every signature. Also: - Reimplemented env::DirectoryWalker / env::forEachEntry / env::Module / env::Process / env::WindowsInfo as Linux-only (uname, /etc/os-release, /proc, std::filesystem) since the Win32 branches that previously held those implementations are gone. - Reduced spawn.cpp / processrunner.cpp / env.cpp to their Linux paths; dropped the helper:: namespace, Win-only waitForAllUSVFSProcesses- WithLock(), Steam Win-registry probe, and Windows mini-dump path. - Linux launcher uses BUILD_JOBS (default 4) so docker rebuilds during the audit don't pin the host. Build verified after every batch with ./build.sh tarball. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Strip Windows-only blocks from env*.cpp/.hSulfurNitride2026-04-286-1785/+295
| | | | | | | | | | | | | | | | | | These files were ported from upstream MO2 with the Windows code wrapped in #ifdef _WIN32 and Linux equivalents in the #else branch. The Windows branches never compile on this fork — drop them so the source matches what's actually built. - envwindows.cpp/.h: kept Linux uname()/os-release implementation; the WindowsInfo::Version/Release fields drop their DWORD/uint32_t fork. - envshell.cpp: deleted entirely (Linux body was empty — header declares no-op stubs inline). - envshell.h: drop Win32 ShellMenu declarations, keep Linux stubs. - envmetrics.cpp: kept QScreen-based Linux Display/Metrics implementation. - envshortcut.cpp: kept Linux .desktop shortcut writer + PE icon extractor, drop Win32 IShellLink path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>