| Commit message (Collapse) | Author | Age | Files | Lines |
| ... | |
| | |
|
| | |
|
| | |
|
| | |
|
| | |
|
| | |
|
| | |
|
| | |
|
| | |
|
| | |
|
| | |
|
| | |
|
| | |
|
| | |
|
| | |
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The Steam build of Ready or Not ships its main executable as
ReadyOrNotSteam-Win64-Shipping.exe; the non-Steam build (Epic / GOG)
ships plain ReadyOrNot-Win64-Shipping.exe. The shipped plugin only
listed the latter, so Steam users hit "game directory not valid"
and couldn't open the instance.
Override looksValid() to accept either binary so detection works
out of the box for both stores. GameBinary defaults to the Steam
variant since Fluorine is Steam/Proton-oriented; the override
makes the non-Steam binary equally valid for instance opening.
Bump plugin version to 0.0.0.2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
"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>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Shrinks the staging tree by ~27 MB (~250M -> ~223M) via two changes in
docker/build-inner.sh:
1. PyQt6 prune. Allowlist QtCore/QtGui/QtWidgets/QtOpenGL/QtOpenGLWidgets
(confirmed by full-repo grep of all shipped Python plugins). Drop the
remaining ~30 .abi3.so bindings plus bindings/, uic/, lupdate/, every
.pyi stub, every __pycache__. Pruning runs BEFORE the existing
ldd-scan loop, so libQt6Designer/Help/Sql/Test/SvgWidgets/Bluetooth/...
stop getting pulled into lib/ as a side effect.
plugins/libs/PyQt6: 31M -> 14M.
2. Dedupe lib/. cp -Lf on globs like libboost_program_options.so* stages
the unversioned soname and the versioned SO as identical real files.
Adds a post-bundling pass that, for each *.so / *.so.N file, finds a
byte-identical versioned twin via cmp -s and replaces it with a
symlink. Skips existing symlinks. Fixes the boost_program_options,
boost_thread, and xcb-cursor duplicates that have been here from the
start.
Also removes the now-orphan fluorine_vfs.dll / fluorine_vfs_hid.dll /
libfluorine_vfs_preload.so staging block left behind after the VFS shim
removal in 2ed0704f.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Dockerfile: drop apt python3/python3-pip, copy the uv static binary from
ghcr.io/astral-sh/uv:latest, install python-build-standalone via
`uv python install` (replacing the manual curl), run aqtinstall via
`uv tool run`, install build-time packages via
`uv pip install --system --break-system-packages --python ...`. Same swap
applied to the CI workflow's inline package-install step.
plugin_python: convert pyproject.toml from [tool.poetry] to PEP 621
[project] + [dependency-groups], drop poetry.lock, replace
abatilo/actions-poetry + `poetry install/run` with astral-sh/setup-uv +
`uv sync/run` in libs/plugin_python/.github/workflows/linting.yml.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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>
|
| |\
| |
| | |
Replace LsLib via proton with native larian-formats Python package
|
| | |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| | |
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>
|
| | |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| | |
When the BG3 metadata path (e.g. AppData/Local/Larian Studios) is a
symlink left over from a previous prefix setup that points at a Steam
compatdata path which no longer resolves, pathlib's
mkdir(parents=True, exist_ok=True) raises FileExistsError — exist_ok
only suppresses the error when the path is an actual directory, and a
dangling symlink fails is_dir(). The launch crashed in
bg3_file_mapper.create_mapping before any mods were deployed.
Walk the path top-down on FileExistsError. For each component that's a
dangling symlink, resolve its target via os.path.realpath and create
the directory there so the symlink becomes valid and the original
mkdir succeeds. Cross-prefix sharing (the reason these symlinks exist)
is preserved.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
| | |
| |
| |
| |
| |
| |
| |
| | |
Contains the following changes:
* Older mod formats (v15 and v16) supported
* Less strict meta.lsx parsing (empty values will use the default for
the type)
* zstd compression support
|
| | | |
|
| | | |
|
| | |
| |
| |
| |
| |
| |
| |
| |
| | |
The builder image is pulled from GHCR and reused across CI runs, so
Dockerfile changes to its pip install line don't take effect until
docker.yml is manually triggered. Move the install into the CI build
step so PRs that add new python deps work without an image rebuild.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
| |/ |
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
After the first launch, doc/Stats -> overwrite/Stats (and siblings) are
deployed as directory symlinks. The next mappings() pass rglob'd
overwrite/ and emitted a Mapping for every nested entry — destinations
resolved back through the parent symlink to the source, so both
create_mapping's unlink+symlink_to and fuseconnector's
create_directory_symlink ended up operating on a path that physically
*was* the source, producing self-pointing symlinks and wiping the
directory contents on the next launch.
Emit one directory mapping per redirected top-level subdir instead. The
parent symlink covers everything underneath via normal path resolution,
so no nested mapping is generated and the alias condition can't arise.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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>
|
| | |
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Local stable builds picked up host CPU flags (AVX-512) and shipped
binaries that crashed on user CPUs without those instructions. CI runs
on generic GitHub-hosted x86-64 runners, so all release builds need to
go through it now.
- Trigger workflow on push of any v* tag in addition to main.
- Resolve channel from the ref: tags/v* -> stable, otherwise beta.
- Tarball name follows channel: fluorine-manager-<version>.tar.gz on
stable, fluorine-manager-beta.tar.gz on beta.
- Beta publish step gated on channel==beta to avoid double-publishing
when a tag also lives on main.
- New "Publish stable release" step creates/updates a release at the
tag, marks it --latest, and attaches the versioned tarball.
To cut v0.2.0: `git tag v0.2.0 && git push origin v0.2.0`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
| |
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>
|
| |
|
|
|
|
|
|
|
| |
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>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
- 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>
|