32 Commits
v0.3.0 ... main

Author SHA1 Message Date
Omni
33b3fbaed2 Release v0.6.0.1 - Hotfix 2026-04-24 19:59:36 +01:00
Omni
2ff09a1448 Release v0.6.0 2026-04-20 20:57:23 +01:00
Omni-guides
69fabb32e6 Update README.md 2026-04-15 20:46:41 +01:00
Omni-guides
6453665620 Update README.md 2026-04-15 16:42:11 +01:00
Omni-guides
cacbbf1fb1 Update README.md 2026-04-15 16:41:47 +01:00
Omni
c3551cd269 Sync from development - prepare for v0.5.0.4 2026-03-29 15:46:37 +01:00
Omni
8e4dd06f11 Sync from development - prepare for v0.5.0.3 2026-03-23 13:46:27 +00:00
Omni
e52e1427f6 Sync from development - prepare for v0.5.0.2 2026-03-15 11:03:28 +00:00
Omni
c294431a35 Sync from development - prepare for v0.5.0.1 2026-03-13 23:04:46 +00:00
Omni
7278efd4cd Remove stale non-premium future plan note 2026-03-13 16:40:25 +00:00
Omni
b29568f590 Clarify non-premium support in README 2026-03-13 16:35:29 +00:00
Omni
3556914560 Sync from development - prepare for v0.5.0 2026-03-13 14:43:25 +00:00
Omni
411addeea2 Sync from development - prepare for v0.4.0 2026-02-25 21:16:02 +00:00
Omni
805718222a Sync from development - prepare for v0.4.0 2026-02-25 20:54:28 +00:00
Omni-guides
2eb54b9a36 Revise README for clarity and additional resources
Updated links and clarified instructions for modlist installation options.
2026-02-24 14:17:12 +00:00
Omni-guides
9cc5245db7 Revise README for clarity and updated features
Updated project description and features in README.
2026-02-24 13:41:18 +00:00
Omni-guides
69738e8e9e Add files via upload 2026-02-24 12:17:07 +00:00
Omni-guides
fdee639734 Delete assets/images/wiki/ModlistGuides/ConfigureNew/new.txt 2026-02-24 11:58:25 +00:00
Omni-guides
b123f6f509 Delete assets/images/wiki/ModlistGuides/ConfigureExisting/new.txt 2026-02-24 11:58:14 +00:00
Omni-guides
368e1bf5ef Add files via upload 2026-02-24 11:57:55 +00:00
Omni-guides
ebf61f67db Add files via upload 2026-02-24 11:57:27 +00:00
Omni-guides
15b90d823c Create new.txt 2026-02-24 11:57:04 +00:00
Omni-guides
309303f721 Create new.txt 2026-02-24 11:56:50 +00:00
Omni-guides
59e03eb38e Add files via upload 2026-02-22 21:30:42 +00:00
Omni-guides
f278b9a8b5 Add files via upload 2026-02-22 21:30:06 +00:00
Omni-guides
9a10812796 Delete assets/images/wiki/ModlistGuides/Jackify/new.txt 2026-02-22 21:29:26 +00:00
Omni-guides
61cfda5dac Add files via upload 2026-02-22 21:29:04 +00:00
Omni-guides
9560c1b72a Delete assets/images/wiki/ModlistGuides/AdditionalTools/new.txt 2026-02-22 21:26:29 +00:00
Omni-guides
5be65c25ac Add files via upload 2026-02-22 21:26:02 +00:00
Omni-guides
053aab04a9 Create new.txt 2026-02-22 21:19:40 +00:00
Omni-guides
f8cdd26d64 Create new.txt 2026-02-22 21:19:20 +00:00
Omni-guides
bc6c0f2e1f Create new.txt 2026-02-22 21:19:02 +00:00
476 changed files with 16493 additions and 6917 deletions

View File

@@ -1,5 +1,167 @@
# Jackify Changelog
## v0.6.0.1 - Hotfix
**Release Date:** 24/04/26
- Resolved some issues with the integration of jackify-engine 0.5.4.
---
## v0.6 - Game Support Expansion, Modding Tool Support, Post-Install Quality
**Release Date:** 20/04/26
### New Game Support
- Additional Game Support - Post-Install automation for BG3, Skyrim VR, and Fallout 4 VR.
- Skyrim VR / Fallout 4 VR: if your modlist needs additional steps you know of, that Jackify does not yet handle, please open an issue on GitHub with your modlist name and the additional steps required. I cannot testing FO4VR directly as I dont own the game.
### Modding Tool Support
- Initial compatibility settings for xEdit, Synthesis, and Pandora are applied automatically during install and configure. Re-apply any time via "Configure Tool Compatibility" in Additional Tasks.
### Steam Shortcut Graphics
- Steam grid artwork now automatically applied to each shortcut, populating all five slots correctly (portrait, landscape, hero, logo, tenfoot).
### First-Launch Reliability
- First Launch Fixes - Skyrim SE modlists should now launch cleanly first time. No more first-launch crash, incorrect AE/CC popup display, initial NXM prompt in MO2, character creation issues, and wrong initial save location.
### Fixes
- Configuration no longer wipes game install paths. Registry writes are now targeted rather than full-prefix replacements.
- Fixed crashes on shutdown caused by force-killing background threads.
### Logging
- Console output reduced to errors only. All informational output goes to the log file and Show Details panel.
### Engine (0.5.4)
- Fixed Nexus sessions silently expiring after installs longer than ~1 hour. The engine now persists refreshed OAuth tokens so you stay logged in across long installs.
- Fixed large downloads hanging indefinitely if a Nexus CDN connection stalled mid-transfer. Downloads now recover automatically and resume from where they left off.
- Removed the disk space pre-flight check, which was incorrectly blocking installs for users with sufficient space. Out-of-disk conditions are still caught and reported if they actually occur.
---
## v0.5.0.4 - Hotfix
**Release Date:** 29/03/26
- Fixed self-update failing silently due to the downloaded archive overwriting the extraction target before the update helper could apply it.
- Engine updated to 0.5.3. NAME_MAX pre-flight check removed — was incorrectly blocking installs on standard filesystems. eCryptFS/fscrypt users still receive an error at the point of failure.
- Fixed Google Drive downloads failing. The Wabbajack CDN proxy was returning a cached broken response for some files; the engine now detects the hash mismatch, retries direct, and constructs a `drive.usercontent.google.com` URL with `confirm=t` to bypass the virus-scan warning page.
- Fixed focus stealing from other windows during the Wine component install phase.
- Fixed a crash on window close from a leaked focus-reclaim timer.
- Baloo file indexer suspended during install and config phases on KDE. No-op elsewhere.
- Fixed Flatpak protontricks install failing on fresh Steam Decks due to Flathub not being registered at user scope.
## v0.5.0.3 - Hotfix
**Release Date:** 23/03/26
- Engine updated to 0.5.2.
- Fixed manual downloads getting stuck on "Browser Opened" when the expected filename has a leading numeric prefix (e.g. `1_filename.zip`) that is absent from the browser-saved file. Both the live download watcher and the startup precheck scan now handle this correctly.
- Fixed "Continue Anyway" on the disk space warning having no effect. The flag was missing from the CLI argument parser, and a separate engine-level registration bug caused it to be rejected regardless. Both are now resolved. The dialog also correctly displays separate download and install space requirements and notes when both paths share the same drive.
- Fixed FNV, FO3, and Enderal modlists losing their game registry paths after configuration. The curated registry files applied during the configuration phase overwrite the Wine prefix registry entirely, wiping the game install paths injected earlier. Jackify now re-injects the correct paths immediately after the curated files are applied.
- Improved detection and guidance for modlists that require the Skyrim Special Edition Creation Kit. If the engine reports missing Creation Kit files, Jackify now surfaces step-by-step instructions for installing and first-launching the Creation Kit via Steam so the required files are in place before retrying.
- Filesystem filename length limit (NAME_MAX) no longer hard-blocks installation on standard filesystems. The check previously triggered incorrectly on ext4/btrfs/XFS. For users on encrypted home directories where the limit is genuinely reduced, Jackify now shows a warning dialog listing the affected files with a "Continue Anyway" option.
- Archive index errors now produce an actionable failure message identifying the specific archive to delete and re-download, rather than a bare engine exception.
- TTW installer temporary working files are now cleaned up after each TTW installation run. These files were previously never removed and could accumulate several GB per install attempt.
- Each GitHub release now includes a `SHA256SUMS` file for verifying your download. See the README for instructions.
## v0.5.0.2 - Hotfix
**Release Date:** 15/03/26
- Disk space warning at install start is no longer a hard block. If the pre-flight check fires before any download or install progress has started, Jackify now shows a warning dialog with the required and available space, a note that modlist updates typically need far less space than a fresh install, and a "Continue Anyway" option. Cancelling still aborts normally.
- Engine: fixed a false-positive in the pre-flight filename length check that could incorrectly trigger on modlist paths using backslash separators.
- Engine: temp folder cleanup at the end of install no longer crashes an otherwise successful installation if a BSA or temp directory is still locked.
## v0.5.0.1 - Hotfix
**Release Date:** 13/03/26
- Fixed Proton prefix creation failing for users who previously had Flatpak Steam installed but have since switched to native Steam.
- Fixed Configure Existing Modlist mangling binary and working directory paths for modlists using a `StockGame` folder (no space variant).
## v0.5.0 - Non-Premium Support, Modlist Update Handling and Overall Reliability Improvements
**Release Date:** 13/03/26
### New in v0.5.0
- Full non-premium install support in both GUI and CLI. Feedback is welcome on this new feature, both positive and negative
- New Jackify Download Manager for Non-Premium accounts, or files Jackify cannot auto-download.
- Improved modlist update handling so existing installs are detected more reliably and Jackify can reuse the existing setup instead of creating duplicate Steam shortcuts.
- Improved Viva New Vegas automation across GUI and CLI paths.
- Improved Wabbajack and Mod Organizer 2 standalone installation workflows.
- Better guidance when Skyrim AE/CC content is missing.
- Further improvements on user-facing logging and error handling
### Manual Download Improvements
- Handles manual downloads more smoothly from start to finish:
- opens required links for you in your system Browser
- watches your download folder
- verifies files and moves them to the correct location automatically,continues with the rest of the modlist install when ready
- Better controls in both GUI and CLI:
- pause/resume download flows, or defer individual archives (useful if one is temporarily unavailable)
- retry deferred items
- reopen file links
- change concurrent browser tab count
- change watch folder
- Deferred items (e.g temporarily unavailable) are retried correctly on later retry/recheck passes.
### Update and Install Reliability
- Worked to improve feature parity between the GUI and CLI frontends, tidying up a few edge cases where CLI behavior did not yet match GUI workflows closely enough.
- Improved update messaging (clearer wording on success/failure).
- Better cancellation handling so stopping a workflow is less likely to leave background processes running.
- Better focus recovery after Steam restart in key workflows.
- Better handling when both Flatpak and native Steam are installed: Jackify now prefers the Steam install that actually contains your installed games, with safe fallback rules if both look valid.
- Install Proton selection now self-heals on startup if the configured Proton was removed, automatically falling back to the best available installed Proton.
- For `/var/home`-based installs (for example Bazzite layouts), ModOrganizer.ini path basis is now aligned so executable/working/game paths resolve correctly.
### Nexus Authentication
- OAuth protocol handler desktop file is now updated if the registered AppImage path no longer matches the current location, preventing silent callback failures after the AppImage is moved or renamed.
- OAuth waiting dialog now includes a "Paste callback URL" button for manual fallback if the browser does not dispatch the jackify:// callback automatically.
### Logging and Error Quality
- Better targeted guidance when required prerequisites or content are missing.
- Improved logging around updater source selection (Nexus/GitHub fallback behavior).
- Better error context while keeping sensitive tokens/keys redacted.
- Install failure fallback now surfaces recent actionable engine output (and resource-limit warnings) instead of only a generic exit-code message.
### Updated jackify-engine to 0.5.0:
- Improved non-premium/manual-download support through a structured manual-download protocol that lets Jackify pause, guide the user, recheck files, and continue installation cleanly once required archives are present.
- Better pre-flight validation before large downloads begin, including earlier checks for game availability, disk space, and filesystem path-length limits.
- More accurate structured error handling for installation failures, with better classification of storage, permission, network, authentication, and validation issues.
---
## v0.4.0 - Error Handling Rewrite
**Release Date:** 2026-02-25
### New Features
- Structured error handling across GUI and CLI with typed `JackifyError` dialogs (clear message, suggested action, numbered recovery steps, optional technical detail).
- Structured engine error receiver: stderr JSON errors are parsed and mapped to user-facing error types, with exit-code fallback.
- Nexus account tier indicator in Settings OAuth (`[Premium]` / `[Free]`) with cached status checks.
- Modlist metadata support via `.jackify_meta.json`, written after install and used by configure workflows.
- TTW eligibility workflow expanded:
- Configure New / Configure Existing can trigger TTW workflow when eligible.
- CLI `configure-modlist` now prompts TTW when eligible.
- FO3 support in configure workflows, including prefix/registry handling.
- Standalone MO2 setup in Additional Tasks (GUI and CLI).
### Bug Fixes
- Proton auto-detection reliability improved, including GE-Proton ranking and fallback behavior.
- Added detection support for system-packaged Proton layouts (Issue #162).
- Download stall false positives reduced by checking byte advancement instead of speed readout alone.
- Flatpak Steam access handling improved with install-directory override support.
- TTW installer output directory is pre-populated to the modlist location.
- Unknown game fallback behavior improved so Wine component installation can continue where appropriate.
### Improvements
- GUI debug log naming standardised to `jackify-debug.log`.
- Error reporting/logging flow cleaned up to improve user facing info and hopefully ease support.
- "Lazy" GUI screen initialization (main menu first, other screens on demand).
- Proton handling improved with Valve Proton fallback when GE-Proton is unavailable.
- FNV/FO3/Enderal registry injection now attempts canonical `C:\Program Files (x86)\Steam\steamapps\common\<Game>` paths via in-prefix symlink, with fallback to real `Z:/D:` paths if symlink creation fails. Looking forward to feedback on this one if anyone still has their FNV launcher only show "Install" instead of "Play".
### Engine Updates
- jackify-engine updated to `0.4.8`.
- Archive download progress improvements (remaining size + ETA).
- Download speed reporting reliability improvements on Linux.
- ZIP extraction fixes for Cyrillic filenames.
---
## v0.3.0 - Codebase Refactoring
**Release Date:** 2026-02-06
@@ -1200,4 +1362,4 @@ This release completes the logging refactor that was blocking development workfl
- Modular handler architecture for extensibility.
## v0.0.09 and Earlier
See commit history for previous versions.
See commit history for previous versions.

172
README.md
View File

@@ -2,162 +2,124 @@
<div align="center">
[Wiki](https://github.com/Omni-guides/Jackify/wiki) | [Nexus](https://www.nexusmods.com/site/mods/1427) | [Download](https://www.nexusmods.com/site/mods/1427?tab=files) | [Wabbajack Discord](https://discord.gg/wabbajack) | [Jackify Issues](https://github.com/Omni-guides/Jackify/issues) | [Legacy Guides](https://github.com/Omni-guides/Jackify/tree/master/Legacy) | [Ko-fi](https://ko-fi.com/omni1)
[Wiki](https://github.com/Omni-guides/Jackify/wiki) | [Nexus](https://www.nexusmods.com/site/mods/1427) | [Download](https://www.nexusmods.com/site/mods/1427?tab=files) | [Wabbajack Discord](https://discord.gg/wabbajack) | [Jackify Issues](https://github.com/Omni-guides/Jackify/issues) | [Ko-fi](https://ko-fi.com/omni1)
</div>
---
# Jackify
A modlist installation and configuration tool for Wabbajack modlists on Linux
Jackify enables seamless installation and configuration of Wabbajack modlists on Linux systems, providing automated Steam shortcut creation and Proton prefix configuration.
### **Repository Migration Notice**
This repository has evolved from the original [Wabbajack-Modlist-Linux](https://github.com/Omni-guides/Wabbajack-Modlist-Linux) guides and bash scripts into **Jackify** - a comprehensive Linux application for Wabbajack modlist management.
**What changed?**
- **From**: Semi-automated bash scripts and step-by-step wiki guides
- **To**: A complete, automated Linux application with GUI and CLI interfaces
- **Why**: To provide a user-friendly application that removes the complexity of Wabbajack and modlist configuration
**Previous Content**: All original guides and scripts are preserved in the `Legacy/` directory. Jackify provides the same functionality with significantly improved automation and user experience.
---
## Introduction
Thank you for your interest in Jackify - the next step, and a giant leap forward from my automated Wabbajack and modlist post-install scripts. So, Jackify - What is it?
Jackify is an almost Linux-native application written in Python, with a GUI produced with PySide6, and a full featured CLI interface if preferred. More info on the "almost" can be found in the full Introduction Wiki page.
**Important Notes for Alpha Users:**
- This is the first alpha release - there WILL be bugs and issues that need to be resolved
- I am not a UI developer, so the current interface is functional but not polished
- Please report any issues you encounter to help improve the application
**Prefer Manual Installation?** If you'd rather use the proven bash scripts and manual guides that have been tested over many months, see the [Legacy Guides](https://github.com/Omni-guides/Jackify/wiki/Legacy-Wiki-Home) for the old installation methods.
Currently, there are two main functions that Jackify will perform at this stage of development:
- Install Wabbajack modlists using jackify-engine (more on jackify-engine in the full Introduction wiki linked above).
- Fully automate the configuration of the Steam shortcut, modlist paths, prefix components, launch options and various other tweaks required to run Wabbajack Modlists on Linux.
- With both of the above combined, Jackify provides an end-to-end modlist installation and configuration process, automatically.
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/D1D8H8WBD)
Jackify is a Linux application for installing and configuring Wabbajack modlists on Linux and Steam Deck. It provides a complete end-to-end workflow — downloading, installing, Steam shortcut creation, Proton prefix setup, and post-install configuration — through both a GUI and a full-featured CLI.
## Features
- Linux-First Python Application: Designed specifically for Linux with minimal external dependencies
- Complete Modlist Workflow: Install from scratch, configure pre-downloaded modlists, or reconfigure existing modlists installations in Steam
- Comprehensive Modlist Support: Support for Skyrim, Fallout 4, Fallout New Vegas, Oblivion, Starfield, Enderal and more
- Automated Steam Integration: Automatic Steam shortcut creation with complete Proton configuration
- Professional Interface: Both GUI and CLI interfaces with identical features
- **Complete Modlist Workflow**: Install from scratch, configure a pre-downloaded modlist, or reconfigure an existing modlist already in Steam
- **Game Support**: Skyrim, Fallout 4, Fallout New Vegas, Oblivion, Starfield, Enderal, and more
- **Automated Steam Integration**: Steam shortcut creation with full Proton configuration
- **GUI and CLI**: Both interfaces provide identical functionality
## Disclaimer
**Jackify is a hobby project in early Alpha development stage. Use at your own risk.**
**Jackify is a hobby project in early Alpha development. Use at your own risk.**
- **No Warranty**: This software is provided "as is" without any warranty or guarantee of functionality
- **Best Effort Support**: Support is provided on a best-effort basis through community channels
- **System Compatibility**: Functionality on your specific system is not guaranteed
- **Data Safety**: Always backup your important data before using Jackify
- **Alpha Software**: Features may be incomplete, unstable, or change without notice
- **Best Effort Support**: Support is provided on a best-effort basis through community channels
- **Data Safety**: Always back up your important data before using Jackify
- **System Compatibility**: Functionality on your specific system is not guaranteed
- **A successful installation does not guarantee a working modlist**: Linux introduces hardware, driver, and system-specific variables that cannot be accounted for. If your modlist installs successfully but does not run correctly, seek help in [#unofficial-linux-help](https://discord.gg/wabbajack) on the Wabbajack Discord — do not contact the modlist author unless they explicitly support Linux
- **Not all modlists can be fully automated**: Some modlists (e.g. Fallout New Vegas lists) require manual steps that Jackify cannot automate (or I have not automated yet). Always check the Install Guide of the Modlist itself to see what could be needed.
- **Most Modlists are not officially supported on Linux**: Jackify makes a best effort to get modlists running, but compatibility is not guaranteed and will vary between modlists, hardware, and system configuration
## Quick Start
## Requirements
### Requirements
- Linux system (Most modern distributions supported)
- Python 3.8+ installed
- Steam installed and configured, Proton Experimental available
- **Nexus Mods Premium subscription** (required for automated downloads)
- Non-premium support planned for future releases
- **FUSE** (required for AppImage execution)
- Pre-installed on most Linux distributions
- If AppImage fails to run, install FUSE using your distribution's package manager
- **Ubuntu/Debian only**: Qt platform plugin library
- Linux system (most modern distributions will work)
- Steam installed and configured — **the Snap version of Steam is not supported**
- **Protontricks** — required for modlist configuration
- See [Installing Additional Tools](https://github.com/Omni-guides/Jackify/wiki/Installing-Additional-Tools#installing-protontricks)
- **GE-Proton 10-14** — While other Proton versions may work, GE-Proton 10-14 is highly recommended for ENB compatibility
- See [Installing Additional Tools](https://github.com/Omni-guides/Jackify/wiki/Installing-Additional-Tools#installing-ge-proton)
- **Nexus Mods account** (Premium required for fully automated downloads; Non-Premium supported with manual browser steps)
- See the [User Guide](https://github.com/Omni-guides/Jackify/wiki/User-Guide) for full details on the options available
- **FUSE2 compatibility (libfuse.so.2) is required for AppImage execution**
- **IF YOU ARE USING an Ubuntu/Debian-based distro** (Ubuntu, Kubuntu, Linux Mint, Pop!_OS, Zorin OS, elementary OS, and others): Qt platform plugin library
- `sudo apt install libxcb-cursor-dev`
- Required for Qt GUI to initialize properly
### Installation
## Installation Quick Start
1. Download the latest release from [Nexus Mods](https://www.nexusmods.com/site/mods/1427?tab=files)
2. Extract the AppImage from the 7z archive
3. Make it executable and run:
```bash
# Download latest release from Nexus Mods
# Extract the Jackify.AppImage from the 7z archive
chmod +x Jackify.AppImage
./Jackify.AppImage
```
## Usage
For CLI mode: `./Jackify.AppImage --cli`
For a complete step-by-step guide with screenshots, see the [User Guide](https://github.com/Omni-guides/Jackify/wiki/User-Guide).
To verify your download, each release includes a `SHA256SUMS` file on the [GitHub releases page](https://github.com/Omni-guides/Jackify/releases/latest). Download it into the same folder as the AppImage, then run:
### Quick Start
```bash
sha256sum -c SHA256SUMS
```
1. **Download**: Get the latest release from [NexusMods](https://www.nexusmods.com/site/mods/1427?tab=files)
2. **Extract**: Unzip the .7z archive to get `Jackify.AppImage`
3. **Run**: `chmod +x Jackify.AppImage && ./Jackify.AppImage`
4. **Install**: Choose "Install a Modlist", select your game and modlist, configure directories and API key
You should see `Jackify.AppImage: OK`. If you see a failure, do not run the file.
**CLI Mode**: Run `./Jackify.AppImage --cli` for command-line interface
For a full step-by-step guide with screenshots, see the [User Guide](https://github.com/Omni-guides/Jackify/wiki/User-Guide).
## Supported Games
- Skyrim Special Edition
- Fallout 4
- Fallout New Vegas
- Oblivion
- Starfield
- Enderal
- Other Games (Cyberpunk 2077, Baldur's Gate 3, and more - Download and Install only for now)
- Other games (Cyberpunk 2077, Baldur's Gate 3, and more — download and install support only for now - full automatioin coming in the future)
## Architecture
Jackify follows a clean separation between frontend and backend:
- Backend Services: Pure business logic with no UI dependencies
- Frontend Interfaces: CLI and GUI implementations using shared backend
- Native Engine: Powered by jackify-engine (custom fork of wabbajack-cli.exe) for optimal performance and compatibility
- Steam Integration: Direct Steam shortcuts.vdf manipulation for creating and modifying Steam shortcuts
- **Backend Services**: Pure business logic with no UI dependencies
- **Frontend Interfaces**: CLI and GUI implementations sharing the same backend
- **Native Engine**: Powered by jackify-engine (custom fork of wabbajack-cli) for optimal Linux performance and compatibility. Texconv for hash-matched texture conversion requires Proton.
- **Steam Integration**: Direct Steam shortcuts.vdf manipulation for shortcut creation and management
## Configuration
Configuration files are stored in:
- Jackify Related: ~/Jackify/
- jackify-engine config: ~/.config/jackify/
## Development
Development and contribution guidelines coming soon.
## License
This project is licensed under the GPLv3 License - see the LICENSE file for details.
All Jackify relted files and configuration data is are stored in `~/Jackify/` and `~/.config/jackify/`.
## Contributing
At this early stage of development, where basic functionality is the primary focus, I'd prefer to use GitHub Issues to suggest improvements, rather tha PRs. This will likely change in the future.
## Future Planned Features (not guaranteed)
At this early stage of development, I'd prefer GitHub Issues for bug reports and suggestions rather than PRs. This will likely change as the project matures. See the CONTRIBUTING document for more details.
- Continue to expand the supported games list for fully automated configuration
- Add full TTW+Modlist automation for TTW based modlists
- Replace the API Key requirement with a more secure OAuth based approach
- Add support for modding and modlist creation tools via a sister application or module
- Revise the GUI to be more refined
- Dark/Light theme support for the GUI
- Advanced logging and diagnostics - more detailed troubleshooting information
- Automatic dependency resolution - ensure all required tools and libraries are installed
## Future Plans (not guaranteed)
- Continue to expand supported games for fully automated configuration
- GUI refinements
- Dark/Light theme support
## Legacy Guides
The original bash scripts and step-by-step manual installation guides are preserved in the [Legacy Guides](https://github.com/Omni-guides/Jackify/wiki/Legacy-Wiki-Home) for those who prefer them or need a fallback.
## License
This project is licensed under the GPLv3 License — see the LICENSE file for details.
## Support
- Issues: Report bugs and request features via GitHub Issues
- Documentation: See the Wiki for detailed guides
- Community: Join the community in the #unofficial-linux-help channel of the Official Wabbajack discord server - https://discord.gg/wabbajack
- **Bugs and feature requests**: [GitHub Issues](https://github.com/Omni-guides/Jackify/issues)
- **Documentation**: [Wiki](https://github.com/Omni-guides/Jackify/wiki)
- **Community**: [#unofficial-linux-help](https://discord.gg/wabbajack) on the Wabbajack Discord
## Acknowledgments
- Wabbajack team for the modlist ecosystem, and wabbajack-cli.exe
- Wabbajack team for the modlist ecosystem and wabbajack-cli
- Linux and Steam Deck gaming communities
- Modlist Authors for their tireless effort in creating modlists in the first place
- Modlist authors for their tireless work
---
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/D1D8H8WBD)
**Jackify** - Simplifying Wabbajack modlist installation and configuration on Linux

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 634 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 439 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 957 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 678 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 685 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 344 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

View File

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

View File

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View File

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 117 KiB

View File

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

View File

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 67 KiB

View File

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 88 KiB

View File

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

View File

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 69 KiB

View File

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

View File

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

View File

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

Before

Width:  |  Height:  |  Size: 130 KiB

After

Width:  |  Height:  |  Size: 130 KiB

View File

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 405 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

View File

Before

Width:  |  Height:  |  Size: 138 KiB

After

Width:  |  Height:  |  Size: 138 KiB

View File

Before

Width:  |  Height:  |  Size: 154 KiB

After

Width:  |  Height:  |  Size: 154 KiB

View File

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 100 KiB

View File

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

View File

Before

Width:  |  Height:  |  Size: 130 KiB

After

Width:  |  Height:  |  Size: 130 KiB

View File

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 405 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

View File

@@ -5,4 +5,4 @@ This package provides both CLI and GUI interfaces for managing
Wabbajack modlists natively on Linux systems.
"""
__version__ = "0.3.0"
__version__ = "0.6.0.1"

View File

@@ -107,7 +107,7 @@ def get_jackify_engine_path():
logger.warning(f"AppImage engine not found at expected path: {engine_path}")
# Priority 3: Check if THIS process is actually running from Jackify AppImage
# (not just inheriting APPDIR from another AppImage like Cursor)
# (not just inheriting APPDIR from another AppImage context)
appdir = os.environ.get('APPDIR')
if appdir and sys.argv[0] and 'jackify' in sys.argv[0].lower() and '/tmp/.mount_' in sys.argv[0]:
# Only use AppImage path if we're actually running a Jackify AppImage
@@ -171,8 +171,7 @@ class ModlistInstallCLI(
self.shortcut_handler = ShortcutHandler(steamdeck=self.steamdeck)
self.context = {}
# Use standard logging (no file handler)
self.logger = logging.getLogger(__name__)
self.logger.propagate = False # Prevent duplicate logs if root logger is also configured
self.logger = logging.getLogger('jackify-cli')
# Initialize Wabbajack parser for game detection
self.wabbajack_parser = WabbajackParser()
@@ -180,6 +179,92 @@ class ModlistInstallCLI(
# Initialize process tracking for cleanup
self._current_process = None
@staticmethod
def _normalize_version_token(value: str | None) -> str | None:
if value is None:
return None
token = str(value).strip()
if not token:
return None
return token.lstrip("vV").lower()
@staticmethod
def _normalize_modlist_name(value: str | None) -> str:
return " ".join((value or "").strip().lower().split())
def _get_requested_modlist_version(self) -> str | None:
info = self.context.get("selected_modlist_info") or {}
return self._normalize_version_token(info.get("version"))
def _evaluate_update_candidate(
self,
modlist_name: str,
install_dir: str,
existing_appid: str | None,
) -> tuple[bool, dict]:
from jackify.backend.utils.modlist_meta import read_modlist_meta
result = {
"eligible": False,
"reason": "unknown",
"requested_version": None,
"installed_version": None,
"version_relation": "unknown",
"installed_name": None,
}
if not existing_appid:
result["reason"] = "missing_shortcut_appid"
return False, result
meta = read_modlist_meta(install_dir)
if not meta:
result["reason"] = "missing_meta"
return False, result
installed_name = (meta.get("modlist_name") or "").strip()
result["installed_name"] = installed_name
if self._normalize_modlist_name(installed_name) != self._normalize_modlist_name(modlist_name):
result["reason"] = "modlist_name_mismatch"
return False, result
requested_version = self._get_requested_modlist_version()
installed_version = self._normalize_version_token(meta.get("modlist_version"))
result["requested_version"] = requested_version
result["installed_version"] = installed_version
if requested_version and installed_version:
result["version_relation"] = "same" if requested_version == installed_version else "different"
result["eligible"] = True
result["reason"] = "eligible"
return True, result
def _find_existing_shortcut_appid(self, modlist_name: str, install_dir: str) -> str | None:
try:
install_real = os.path.realpath(install_dir)
candidate_exes = [
os.path.join(install_real, "ModOrganizer.exe"),
os.path.join(install_real, "files", "ModOrganizer.exe"),
]
for exe_path in candidate_exes:
if not os.path.exists(exe_path):
continue
appid = self.shortcut_handler.get_appid_from_vdf(modlist_name, exe_path)
if appid:
return appid
for shortcut in self.shortcut_handler.find_shortcuts_by_exe("ModOrganizer.exe"):
if (
shortcut.get("AppName", "").strip() == modlist_name.strip()
and os.path.realpath(shortcut.get("StartDir", "")) == install_real
):
raw_appid = shortcut.get("appid")
if raw_appid is not None:
return str(int(raw_appid) & 0xFFFFFFFF)
except Exception as e:
self.logger.warning("CLI update detection: failed shortcut lookup: %s", e)
return None
def cleanup(self):
"""Clean up any running jackify-engine process"""
if self._current_process and self._current_process.poll() is None:
@@ -238,4 +323,3 @@ class ModlistInstallCLI(
print(auth_display)
print(f"{COLOR_INFO}----------------------------------------{COLOR_RESET}")

View File

@@ -1,4 +1,5 @@
"""CLI configuration phase methods for ModlistInstallCLI (Mixin)."""
import json
import logging
import os
import subprocess
@@ -120,14 +121,16 @@ class ModlistOperationsConfigurationCLIMixin:
if debug_mode:
cmd.append('--debug')
self.logger.info("Adding --debug flag to jackify-engine")
writeback_path = str(auth_service.get_token_writeback_path())
original_env_values = {
'NEXUS_API_KEY': os.environ.get('NEXUS_API_KEY'),
'NEXUS_OAUTH_INFO': os.environ.get('NEXUS_OAUTH_INFO'),
'JACKIFY_TOKEN_WRITEBACK': os.environ.get('JACKIFY_TOKEN_WRITEBACK'),
'DOTNET_SYSTEM_GLOBALIZATION_INVARIANT': os.environ.get('DOTNET_SYSTEM_GLOBALIZATION_INVARIANT')
}
try:
os.environ['JACKIFY_TOKEN_WRITEBACK'] = writeback_path
if oauth_info:
os.environ['NEXUS_OAUTH_INFO'] = oauth_info
from jackify.backend.services.nexus_oauth_service import NexusOAuthService
@@ -166,18 +169,81 @@ class ModlistOperationsConfigurationCLIMixin:
from jackify.backend.handlers.subprocess_utils import get_clean_subprocess_env
clean_env = get_clean_subprocess_env()
self._current_process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=False, env=clean_env, cwd=engine_dir)
self._current_process = subprocess.Popen(
cmd,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=False,
env=clean_env,
cwd=engine_dir,
)
proc = self._current_process
def _write_stdin(payload: str) -> bool:
if not proc.stdin or proc.poll() is not None:
return False
try:
proc.stdin.write((payload + '\n').encode('utf-8'))
proc.stdin.flush()
return True
except Exception:
self.logger.debug("Failed writing to engine stdin", exc_info=True)
return False
buffer = b''
inline_progress_active = False
pending_manual = []
while True:
chunk = proc.stdout.read(1)
if not chunk:
break
buffer += chunk
if chunk == b'\n':
if chunk in (b'\n', b'\r'):
line = buffer.decode('utf-8', errors='replace')
decoded = line.rstrip('\r\n')
if decoded.startswith('{'):
try:
event = json.loads(decoded)
except (json.JSONDecodeError, ValueError):
event = None
if event:
event_name = event.get('event')
if event_name == 'manual_download_required':
pending_manual.append(event)
buffer = b''
continue
if event_name == 'manual_download_list_complete':
loop_iter = event.get('loop_iteration', 1)
for item in pending_manual:
item['loop_iteration'] = loop_iter
from jackify.backend.handlers.config_handler import ConfigHandler
raw_limit = ConfigHandler().get('manual_download_concurrent_limit', 2)
try:
manual_limit = int(raw_limit)
except (TypeError, ValueError):
manual_limit = 2
from jackify.frontends.cli.commands.manual_download_flow import run_cli_manual_download_phase
completed = run_cli_manual_download_phase(
events=list(pending_manual),
loop_iteration=loop_iter,
download_dir=actual_download_path,
stdin_write=_write_stdin,
concurrent_limit=max(1, min(5, manual_limit)),
)
if not completed:
if proc.poll() is None:
proc.terminate()
buffer = b''
break
pending_manual.clear()
buffer = b''
continue
if event_name == 'manual_download_phase_complete':
print("All manual downloads confirmed. Resuming installation...")
buffer = b''
continue
if '[FILE_PROGRESS]' in line:
parts = line.split('[FILE_PROGRESS]', 1)
if parts[0].strip():
@@ -185,19 +251,16 @@ class ModlistOperationsConfigurationCLIMixin:
else:
buffer = b''
continue
print(line, end='')
buffer = b''
elif chunk == b'\r':
line = buffer.decode('utf-8', errors='replace')
if '[FILE_PROGRESS]' in line:
parts = line.split('[FILE_PROGRESS]', 1)
if parts[0].strip():
line = parts[0].rstrip()
else:
buffer = b''
continue
print(line, end='')
sys.stdout.flush()
clean_line = line.rstrip('\r\n')
if clean_line.startswith("Installing files "):
print(f"\r{clean_line}", end='')
sys.stdout.flush()
inline_progress_active = True
else:
if inline_progress_active:
print()
inline_progress_active = False
print(line, end='')
buffer = b''
if buffer:
@@ -209,10 +272,17 @@ class ModlistOperationsConfigurationCLIMixin:
else:
line = ''
if line:
if inline_progress_active:
print()
inline_progress_active = False
print(line, end='')
if inline_progress_active:
print()
proc.wait()
self._current_process = None
auth_service.apply_token_writeback(writeback_path)
if proc.returncode != 0:
print(f"{COLOR_ERROR}Jackify Install Engine exited with code {proc.returncode}.{COLOR_RESET}")
self.logger.error(f"Engine exited with code {proc.returncode}.")
@@ -343,7 +413,10 @@ class ModlistOperationsConfigurationCLIMixin:
if not is_gui_mode:
self.logger.debug("configuration_phase: Not in GUI mode, prompting user for configuration...")
print("\n" + "-" * 28)
print(f"{COLOR_PROMPT}Would you like to add '{shortcut_name}' to Steam and configure it now?{COLOR_RESET}")
print(
f"{COLOR_PROMPT}Would you like to add '{shortcut_name}' to Steam and configure it now? "
f"Steam will restart and close any running game.{COLOR_RESET}"
)
configure_choice = input(f"{COLOR_PROMPT}Configure now? (Y/n): {COLOR_RESET}").strip().lower()
self.logger.debug(f"configuration_phase: User choice: '{configure_choice}'")
@@ -373,6 +446,16 @@ class ModlistOperationsConfigurationCLIMixin:
app_id = None
use_automated_prefix = os.environ.get('JACKIFY_USE_AUTOMATED_PREFIX', '1') == '1'
existing_shortcut_appid = self.context.get('existing_shortcut_appid')
update_existing_install = bool(self.context.get('update_existing_install'))
if update_existing_install and existing_shortcut_appid:
app_id = str(existing_shortcut_appid)
success = True
prefix_path = None
result = True
print(f"\n{COLOR_INFO}Update mode selected. Reusing existing Steam shortcut AppID {app_id}.{COLOR_RESET}")
use_automated_prefix = False
if use_automated_prefix:
print(f"\n{COLOR_INFO}Using automated Steam setup workflow...{COLOR_RESET}")
@@ -383,11 +466,30 @@ class ModlistOperationsConfigurationCLIMixin:
start_time = time.time()
def progress_callback(message):
noisy_patterns = (
"using bundled tools directory",
"bundled tools available",
"checking winetricks dependencies",
"(bundled)",
"(system)",
"wget",
"curl",
"aria2c",
"sha256sum",
"cabextract",
)
message_lc = message.lower()
if any(pattern in message_lc for pattern in noisy_patterns):
# Keep dependency/tool chatter in logs only for CLI readability.
self.logger.debug("Automated prefix detail: %s", message)
return
elapsed = time.time() - start_time
hours = int(elapsed // 3600)
minutes = int((elapsed % 3600) // 60)
seconds = int(elapsed % 60)
timestamp = f"[{hours:02d}:{minutes:02d}:{seconds:02d}]"
self.logger.info("Automated prefix progress: %s", message)
print(f"{COLOR_INFO}{timestamp} {message}{COLOR_RESET}")
try:
@@ -489,17 +591,50 @@ class ModlistOperationsConfigurationCLIMixin:
success, prefix_path, app_id = True, None, None
else:
success, prefix_path, app_id = False, None, None
if success:
if success:
if update_existing_install and app_id:
print(f"{COLOR_SUCCESS}Update mode Steam setup confirmed.{COLOR_RESET}")
print(f"{COLOR_INFO}Reusing Steam AppID: {app_id}{COLOR_RESET}")
# Apply artwork and restart Steam -- skipped in update path since the full
# workflow is bypassed, but artwork and Steam state still need refreshing.
_game_type = self.context.get('detected_game') or self.context.get('special_game_type')
try:
from jackify.backend.handlers.modlist_handler import ModlistHandler
ModlistHandler().set_steam_grid_images(str(app_id), install_dir_str, game_type=_game_type)
except Exception as e:
self.logger.warning("Failed to apply Steam artwork in update mode: %s", e)
if _game_type == 'cp2077':
# CP2077 launch options may be absent on lists originally installed
# under v0.5 before CP2077 support was added.
try:
from jackify.backend.handlers.shortcut_handler import ShortcutHandler
from jackify.backend.handlers.config_handler import ConfigHandler
sh = ShortcutHandler(
config_handler=ConfigHandler(),
steamdeck=bool(self.system_info and self.system_info.is_steamdeck),
)
sh.update_shortcut_launch_options(
shortcut_name,
mo2_exe_path,
'WINEDLLOVERRIDES="version=n,b;winmm=n,b" %command%',
)
except Exception as e:
self.logger.warning("Failed to update CP2077 launch options in update mode: %s", e)
try:
from jackify.backend.services.automated_prefix_service import AutomatedPrefixService
AutomatedPrefixService(self.system_info).restart_steam()
except Exception as e:
self.logger.warning("Failed to restart Steam in update mode: %s", e)
else:
print(f"{COLOR_SUCCESS}Automated Steam setup completed successfully!{COLOR_RESET}")
if prefix_path:
print(f"{COLOR_INFO}Proton prefix created at: {prefix_path}{COLOR_RESET}")
if app_id:
print(f"{COLOR_INFO}Steam AppID: {app_id}{COLOR_RESET}")
else:
print(f"{COLOR_ERROR}Automated Steam setup failed. Result: {result}{COLOR_RESET}")
print(f"{COLOR_ERROR}Steam integration was not completed. Please check the logs for details.{COLOR_RESET}")
return
else:
print(f"{COLOR_ERROR}Automated Steam setup failed. Result: {result}{COLOR_RESET}")
print(f"{COLOR_ERROR}Steam integration was not completed. Please check the logs for details.{COLOR_RESET}")
return
from jackify.backend.services.modlist_service import ModlistService
from jackify.backend.models.modlist import ModlistContext
@@ -526,14 +661,93 @@ class ModlistOperationsConfigurationCLIMixin:
progress_callback("")
progress_callback("=== Configuration Phase ===")
print(f"\n{COLOR_INFO}=== Configuration Phase ==={COLOR_RESET}")
self.logger.info("Running post-installation configuration phase using ModlistService")
print(f"\n{COLOR_INFO}=== Configuration Phase ==={COLOR_RESET}")
self.logger.info("Running post-installation configuration phase using ModlistService")
configuration_success = modlist_service.configure_modlist_post_steam(modlist_context)
if configuration_success:
print(f"{COLOR_SUCCESS}Configuration completed successfully!{COLOR_RESET}")
self.logger.info("Post-installation configuration completed successfully")
print(f"{COLOR_INFO}Core configuration complete. Checking post-install automation...{COLOR_RESET}")
try:
# Ensure CLI install flow gets the same VNV automation behavior as GUI.
from jackify.backend.services.vnv_integration_helper import (
run_vnv_automation_if_applicable,
should_offer_vnv_automation,
)
from jackify.backend.services.automated_prefix_service import AutomatedPrefixService
from jackify.backend.services.vnv_post_install_service import VNVPostInstallService
from jackify.backend.handlers.path_handler import PathHandler
from jackify.frontends.cli.commands.vnv_manual_downloads import (
build_vnv_cli_manual_file_callback,
create_vnv_cli_progress_callback,
ensure_vnv_cli_manual_downloads,
)
modlist_name_for_automation = self.context.get('modlist_name') or shortcut_name or ""
def _confirm_vnv(description: str) -> bool:
print(f"\n{description}\n")
try:
user_input = input(f"{COLOR_PROMPT}Run VNV post-install automation now? (Y/n): {COLOR_RESET}").strip().lower()
except (EOFError, KeyboardInterrupt):
return False
return user_input in ("", "y", "yes")
install_path = Path(install_dir_str)
if should_offer_vnv_automation(modlist_name_for_automation, install_path):
game_paths = PathHandler().find_vanilla_game_paths()
resolved_game_root = game_paths.get('Fallout New Vegas')
vnv_service = VNVPostInstallService(
modlist_install_location=install_path,
game_root=resolved_game_root or install_path,
ttw_installer_path=AutomatedPrefixService.get_ttw_installer_path(),
)
completed = vnv_service.check_already_completed()
all_vnv_steps_done = (
completed['root_mods']
and completed['4gb_patch']
and completed['bsa_decompressed']
)
if all_vnv_steps_done:
print(f"{COLOR_INFO}VNV post-install steps are already complete.{COLOR_RESET}")
elif _confirm_vnv(vnv_service.get_automation_description()):
if not ensure_vnv_cli_manual_downloads(vnv_service, output_callback=print):
print(f"{COLOR_WARNING}VNV manual downloads were not completed. Skipping VNV automation.{COLOR_RESET}")
else:
progress_callback, close_progress = create_vnv_cli_progress_callback(print)
try:
automation_ran, vnv_error = run_vnv_automation_if_applicable(
modlist_name=modlist_name_for_automation,
modlist_install_location=install_path,
game_root=None, # Auto-detect from modlist structure.
ttw_installer_path=AutomatedPrefixService.get_ttw_installer_path(),
progress_callback=progress_callback,
manual_file_callback=build_vnv_cli_manual_file_callback(vnv_service, output_callback=print),
confirmation_callback=lambda _description: True,
)
finally:
close_progress()
if automation_ran and not vnv_error:
print(f"{COLOR_INFO}VNV post-install automation completed.{COLOR_RESET}")
if vnv_error:
print(f"{COLOR_WARNING}VNV automation encountered an error: {vnv_error}{COLOR_RESET}")
print(f"{COLOR_INFO}You can complete these steps manually by following: https://vivanewvegas.moddinglinked.com/wabbajack.html{COLOR_RESET}")
else:
print(f"{COLOR_INFO}VNV automation skipped by user.{COLOR_RESET}")
except Exception as vnv_err:
self.logger.error("VNV post-install automation failed: %s", vnv_err, exc_info=True)
print(f"{COLOR_WARNING}VNV automation could not be completed. Check logs for details.{COLOR_RESET}")
try:
# v0.4.0 contract: offer TTW flow for eligible FNV lists (e.g., Begin Again).
from jackify.backend.handlers.modlist_install_cli_ttw import prompt_ttw_if_eligible
prompt_ttw_if_eligible(
install_dir_str,
self.context.get('modlist_name') or shortcut_name or "",
)
except Exception as ttw_err:
self.logger.error("TTW post-install prompt failed: %s", ttw_err, exc_info=True)
print(f"{COLOR_WARNING}TTW integration prompt failed. Check logs for details.{COLOR_RESET}")
print(f"{COLOR_SUCCESS}Configuration completed successfully!{COLOR_RESET}")
else:
print(f"{COLOR_WARNING}Configuration had some issues but completed.{COLOR_RESET}")
self.logger.warning("Post-installation configuration had issues")

View File

@@ -68,7 +68,7 @@ class ModlistOperationsConfigurationGUIMixin:
if result:
if completion_callback:
completion_callback(True, "Configuration completed successfully!", config_context['name'])
completion_callback(True, "Core configuration complete", config_context['name'])
return True
else:
retry_count += 1
@@ -139,7 +139,7 @@ class ModlistOperationsConfigurationGUIMixin:
if result:
if completion_callback:
completion_callback(True, "Configuration completed successfully!", config_context['name'])
completion_callback(True, "Core configuration complete", config_context['name'])
return True
else:
if progress_callback:

View File

@@ -243,6 +243,46 @@ class ModlistOperationsDiscoveryMixin:
self.context['download_dir'] = download_dir_path
self.logger.debug(f"Download directory context set to: {self.context['download_dir']}")
install_dir_value = self.context.get('install_dir')
install_dir_real = os.path.realpath(str(install_dir_value[0] if isinstance(install_dir_value, tuple) else install_dir_value))
existing_appid = self._find_existing_shortcut_appid(self.context['modlist_name'], install_dir_real)
eligible_update, update_meta = self._evaluate_update_candidate(
self.context['modlist_name'],
install_dir_real,
existing_appid,
)
if eligible_update:
print("\n" + "-" * 28)
print(f"{COLOR_WARNING}Existing modlist installation detected in this directory.{COLOR_RESET}")
relation = update_meta.get("version_relation")
if relation == "different":
print(
f"{COLOR_INFO}Detected version change: installed v{update_meta.get('installed_version')} -> "
f"selected v{update_meta.get('requested_version')}.{COLOR_RESET}"
)
elif relation == "same" and update_meta.get("installed_version"):
print(
f"{COLOR_INFO}Detected same version (v{update_meta.get('installed_version')}). "
"Use update mode for repair/reconfigure behavior." + f"{COLOR_RESET}"
)
print("Choose how to proceed:")
print(" 1. Update existing install (recommended)")
print(" 2. New install with a different Steam shortcut name")
print(" 0. Cancel")
update_choice = input(f"{COLOR_PROMPT}Enter your selection (0-2): {COLOR_RESET}").strip()
if update_choice == "1":
self.context['update_existing_install'] = True
self.context['existing_shortcut_appid'] = existing_appid
self.logger.info("CLI update mode selected; reusing AppID %s", existing_appid)
elif update_choice == "2":
print(
f"{COLOR_WARNING}For a new install, choose a different Modlist Name before proceeding.{COLOR_RESET}"
)
return None
else:
self.logger.info("User cancelled at CLI update detection prompt.")
return None
if 'nexus_api_key' not in self.context or not self.context.get('nexus_api_key'):
from jackify.backend.services.nexus_auth_service import NexusAuthService
auth_service = NexusAuthService()

View File

@@ -17,6 +17,10 @@ from typing import Optional
from .config_handler_encryption import ConfigEncryptionMixin
from .config_handler_directories import ConfigDirectoriesMixin
from .config_handler_proton import ConfigProtonMixin
from jackify.shared.steam_utils import (
STEAM_PREFERENCE_AUTO,
resolve_preferred_steam_installation,
)
logger = logging.getLogger(__name__)
@@ -50,6 +54,7 @@ class ConfigHandler(ConfigEncryptionMixin, ConfigDirectoriesMixin, ConfigProtonM
"resolution": None,
"protontricks_path": None,
"steam_path": None,
"steam_install_preference": STEAM_PREFERENCE_AUTO, # auto|flatpak|native
"nexus_api_key": None, # Base64 encoded API key
"default_install_parent_dir": None, # Parent directory for modlist installations
"default_download_parent_dir": None, # Parent directory for downloads
@@ -62,8 +67,10 @@ class ConfigHandler(ConfigEncryptionMixin, ConfigDirectoriesMixin, ConfigProtonM
"proton_path": None, # Install Proton path (for jackify-engine) - None means auto-detect
"proton_version": None, # Install Proton version name - None means auto-detect
"steam_restart_strategy": "jackify", # "jackify" (default) or "simple"
"manual_download_concurrent_limit": 2, # Shared GUI/CLI default for manual download browser tabs
"manual_download_watch_directory": None, # Optional override for manual-download watcher folder
"window_width": None, # Saved window width (None = use dynamic sizing)
"window_height": None # Saved window height (None = use dynamic sizing)
"window_height": None, # Saved window height (None = use dynamic sizing)
}
# Load configuration if exists
@@ -72,14 +79,13 @@ class ConfigHandler(ConfigEncryptionMixin, ConfigDirectoriesMixin, ConfigProtonM
# Perform version migrations
self._migrate_config()
# Normalize/repair Proton selections on every startup so stale deleted versions
# cannot break workflows.
self.normalize_proton_paths_on_boot()
# If steam_path is not set, detect it
if not self.settings["steam_path"]:
self.settings["steam_path"] = self._detect_steam_path()
# Auto-detect and set Proton version ONLY on first run (config file doesn't exist)
# Do NOT overwrite user's saved settings!
if not os.path.exists(self.config_file) and not self.settings.get("proton_path"):
self._auto_detect_proton()
# If jackify_data_dir is not set, initialize it to default
if not self.settings.get("jackify_data_dir"):
@@ -95,35 +101,16 @@ class ConfigHandler(ConfigEncryptionMixin, ConfigDirectoriesMixin, ConfigProtonM
str: Path to the Steam installation or None if not found
"""
logger.info("Detecting Steam installation path...")
# Common Steam installation paths
steam_paths = [
os.path.expanduser("~/.steam/steam"),
os.path.expanduser("~/.local/share/Steam"),
os.path.expanduser("~/.steam/root")
]
# Check each path
for path in steam_paths:
if os.path.exists(path):
logger.info(f"Found Steam installation at: {path}")
return path
# If not found in common locations, try to find using libraryfolders.vdf
libraryfolders_vdf_paths = [
os.path.expanduser("~/.steam/steam/config/libraryfolders.vdf"),
os.path.expanduser("~/.local/share/Steam/config/libraryfolders.vdf"),
os.path.expanduser("~/.steam/root/config/libraryfolders.vdf"),
os.path.expanduser("~/.var/app/com.valvesoftware.Steam/.local/share/Steam/config/libraryfolders.vdf") # Flatpak
]
for vdf_path in libraryfolders_vdf_paths:
if os.path.exists(vdf_path):
# Extract the Steam path from the libraryfolders.vdf path
steam_path = os.path.dirname(os.path.dirname(vdf_path))
logger.info(f"Found Steam installation at: {steam_path}")
return steam_path
preference = self.settings.get("steam_install_preference", STEAM_PREFERENCE_AUTO)
install_type, install_root = resolve_preferred_steam_installation(preference=preference)
if install_root:
logger.info(
"Selected Steam installation: %s (%s)",
install_type,
install_root,
)
return str(install_root)
logger.error("Steam installation not found")
return None
@@ -376,4 +363,4 @@ class ConfigHandler(ConfigEncryptionMixin, ConfigDirectoriesMixin, ConfigProtonM

View File

@@ -3,6 +3,8 @@ Config handler Proton path and version getters and auto-detect.
"""
import logging
from pathlib import Path
from typing import Optional, Dict, Any
logger = logging.getLogger(__name__)
@@ -10,6 +12,105 @@ logger = logging.getLogger(__name__)
class ConfigProtonMixin:
"""Mixin providing Proton path/version and auto-detect for ConfigHandler."""
@staticmethod
def _is_usable_proton_path(proton_path: Optional[str]) -> bool:
"""Return True when path looks like a valid Proton install directory."""
if not proton_path:
return False
try:
p = Path(str(proton_path)).expanduser()
if not p.is_dir():
return False
# Valve Proton structure
if (p / "dist" / "bin" / "wine").exists():
return True
# GE-Proton structure
if (p / "files" / "bin" / "wine").exists():
return True
return False
except Exception:
return False
@staticmethod
def _best_proton_entry() -> Optional[Dict[str, Any]]:
"""Get best detected Proton entry or None."""
try:
from .wine_utils import WineUtils
return WineUtils.select_best_proton()
except Exception:
return None
def normalize_proton_paths_on_boot(self) -> bool:
"""
Ensure stored Proton paths are valid at startup, repairing stale selections.
Rules:
- If install proton path is missing/invalid, auto-detect next best and persist it.
- If no compatible Proton exists, persist install path/version as null.
- If game proton path is set and invalid, reset it to install proton (or null).
Returns:
True if config values were changed and saved, False otherwise.
"""
changed = False
install_path = self.settings.get("proton_path")
if install_path == "auto":
install_path = None
install_valid = self._is_usable_proton_path(install_path)
if not install_valid:
best = self._best_proton_entry()
if best:
best_path = str(best["path"])
best_name = str(best.get("name") or Path(best_path).name)
if self.settings.get("proton_path") != best_path:
self.settings["proton_path"] = best_path
changed = True
if self.settings.get("proton_version") != best_name:
self.settings["proton_version"] = best_name
changed = True
logger.warning(
"Install Proton path was missing/invalid; auto-selected %s (%s)",
best_name,
best_path,
)
else:
if self.settings.get("proton_path") is not None:
self.settings["proton_path"] = None
changed = True
if self.settings.get("proton_version") is not None:
self.settings["proton_version"] = None
changed = True
logger.warning(
"Install Proton path was missing/invalid and no compatible Proton was found"
)
else:
# Keep proton_version in sync with existing valid path when missing/legacy.
if not self.settings.get("proton_version"):
self.settings["proton_version"] = Path(str(install_path)).name
changed = True
effective_install = self.settings.get("proton_path")
game_path = self.settings.get("game_proton_path")
# Legacy/placeholder values should not persist for runtime resolution.
if game_path in ("same_as_install", "auto"):
target = effective_install
if self.settings.get("game_proton_path") != target:
self.settings["game_proton_path"] = target
changed = True
elif game_path and not self._is_usable_proton_path(game_path):
self.settings["game_proton_path"] = effective_install
changed = True
logger.warning(
"Game Proton path was missing/invalid; reset to install Proton path"
)
if changed:
self.save_config()
return changed
def get_proton_path(self):
"""Retrieve the saved Install Proton path. Always reads fresh from disk."""
try:

View File

@@ -92,7 +92,7 @@ class EnginePerformanceMonitor:
# Also monitor the parent Python process for comparison
try:
self._parent_process = psutil.Process(os.getpid())
except:
except Exception:
self._parent_process = None
self._monitoring = True
@@ -220,7 +220,7 @@ class EnginePerformanceMonitor:
parent_cpu_percent = self._parent_process.cpu_percent()
parent_memory_info = self._parent_process.memory_info()
parent_memory_mb = parent_memory_info.rss / (1024 * 1024)
except:
except Exception:
pass
# Get I/O info

View File

@@ -521,11 +521,13 @@ class FileSystemHandler(FilesystemDownloadMixin, FilesystemOwnershipMixin, Files
# Game-specific Documents directory names (for both Linux home and Wine prefix)
game_docs_dirs = {
"skyrimse": "Skyrim Special Edition",
"skyrimvr": "Skyrim VR",
"fallout4": "Fallout4",
"fallout4vr": "Fallout4VR",
"falloutnv": "FalloutNV",
"oblivion": "Oblivion",
"enderal": "Enderal Special Edition",
"enderalse": "Enderal Special Edition"
"enderalse": "Enderal Special Edition",
}
game_dirs = {
@@ -561,41 +563,193 @@ class FileSystemHandler(FilesystemDownloadMixin, FilesystemOwnershipMixin, Files
os.makedirs(dir_path, exist_ok=True)
self.logger.debug(f"Created game-specific directory: {dir_path}")
# CRITICAL: Create game-specific Documents directories in Wine prefix
# CP2077 and BG3 use AppData/Local only (no My Games)
appdata_only_dirs = {
"cp2077": os.path.join("CD Projekt Red", "Cyberpunk 2077"),
"bg3": os.path.join("Larian Studios", "Baldur's Gate 3"),
}
# CRITICAL: Create game-specific directories in Wine prefix
# Required for USVFS to virtualize profile INIs on first launch
if game_name in game_docs_dirs:
docs_dir_name = game_docs_dirs[game_name]
# Find compatdata path for this AppID
from ..handlers.path_handler import PathHandler
path_handler = PathHandler()
compatdata_path = path_handler.find_compat_data(appid)
if compatdata_path:
# Create Documents/My Games/{GameName} in Wine prefix
wine_docs_path = os.path.join(
str(compatdata_path),
"pfx",
"drive_c",
"users",
"steamuser",
"Documents",
"My Games",
docs_dir_name
from ..handlers.path_handler import PathHandler
path_handler = PathHandler()
compatdata_path = path_handler.find_compat_data(appid)
if compatdata_path:
prefix_user = os.path.join(
str(compatdata_path), "pfx", "drive_c", "users", "steamuser"
)
if game_name in appdata_only_dirs:
appdata_path = os.path.join(
prefix_user, "AppData", "Local", appdata_only_dirs[game_name]
)
try:
os.makedirs(appdata_path, exist_ok=True)
self.logger.info(f"Created Wine prefix AppData/Local directory: {appdata_path}")
except Exception as e:
self.logger.warning(f"Could not create AppData/Local directory {appdata_path}: {e}")
elif game_name in game_docs_dirs:
docs_dir_name = game_docs_dirs[game_name]
wine_docs_path = os.path.join(
prefix_user, "Documents", "My Games", docs_dir_name
)
try:
os.makedirs(wine_docs_path, exist_ok=True)
self.logger.info(f"Created Wine prefix Documents directory for USVFS: {wine_docs_path}")
self.logger.debug(f"This allows USVFS to virtualize profile INI files on first launch")
self.logger.info(f"Created Wine prefix Documents directory: {wine_docs_path}")
except Exception as e:
self.logger.warning(f"Could not create Wine prefix Documents directory {wine_docs_path}: {e}")
# Don't fail completely - this is a first-launch optimization
else:
self.logger.warning(f"Could not find compatdata path for AppID {appid}, skipping Wine prefix Documents directory creation")
self.logger.debug("Wine prefix Documents directories will be created when game runs for first time")
if game_name == "skyrimse":
self._seed_skyrim_first_launch_files(prefix_user, docs_dir_name)
elif game_name == "fallout4":
self._seed_fo4_first_launch_files(prefix_user, docs_dir_name)
elif game_name == "skyrimvr":
self._seed_skyrimvr_first_launch_files(prefix_user, docs_dir_name)
elif game_name == "fallout4vr":
self._seed_fallout4vr_first_launch_files(prefix_user, docs_dir_name)
else:
self.logger.warning(f"Could not find compatdata path for AppID {appid}, skipping Wine prefix directory creation")
return True
except Exception as e:
self.logger.error(f"Error creating required directories: {e}")
return False
def _seed_skyrim_first_launch_files(self, prefix_user: str, docs_dir_name: str) -> None:
"""
Pre-seed files in the Wine prefix that Skyrim SE/AE needs on first launch.
Two files must exist before first launch to avoid USVFS and engine issues:
1. AppData/Local/Skyrim Special Edition/Plugins.txt - empty anchor file.
USVFS builds its VFS tree at MO2 startup. If this path does not exist,
USVFS logs the directory as missing and skips adding Plugins.txt to the
initial tree. It then tries to reroute the file dynamically, but a mutex
deadlock (thread never releases the write mutex on first launch) blocks
the reroute. The game falls through to the real filesystem, finds no
Plugins.txt, and loads only base-game ESPs - causing a null form crash
for any SKSE plugin that expects modlist ESPs (e.g. BladeAndBlunt.dll).
On second launch the directory exists, USVFS initialises correctly, no crash.
Pre-seeding an empty file gives USVFS its anchor; content is irrelevant
because USVFS reroutes reads to the active MO2 profile's plugins.txt anyway.
2. Documents/My Games/Skyrim Special Edition/SkyrimPrefs.ini - minimal stub.
The CC/AE download prompt is triggered by bDownloadCC=0 (or absent) in
SkyrimPrefs.ini. This check fires before PrivateProfileRedirector (PPR)
hooks the Windows INI API, so the game reads the real prefix path directly,
not the MO2 profile version. A minimal stub with bDownloadCC=1 suppresses
the prompt. PPR redirects all subsequent reads to the active profile once
it loads, so this stub is never read again after early engine init.
Only created if the file does not already exist.
"""
# Fix 1: empty Plugins.txt anchor for USVFS
appdata_sse = os.path.join(prefix_user, "AppData", "Local", "Skyrim Special Edition")
plugins_txt = os.path.join(appdata_sse, "Plugins.txt")
try:
os.makedirs(appdata_sse, exist_ok=True)
if not os.path.exists(plugins_txt):
open(plugins_txt, 'w').close()
self.logger.info(f"Created Plugins.txt anchor for USVFS: {plugins_txt}")
else:
self.logger.debug(f"Plugins.txt already exists, skipping: {plugins_txt}")
except Exception as e:
self.logger.warning(f"Could not create Plugins.txt anchor: {e}")
# Fix 2: minimal SkyrimPrefs.ini at real Documents path to suppress AE popup
skyrimprefs_path = os.path.join(
prefix_user, "Documents", "My Games", docs_dir_name, "SkyrimPrefs.ini"
)
try:
if not os.path.exists(skyrimprefs_path):
with open(skyrimprefs_path, 'w', encoding='utf-8') as f:
f.write("[General]\nbDownloadCC=1\n")
self.logger.info(f"Created SkyrimPrefs.ini stub to suppress AE popup: {skyrimprefs_path}")
else:
self.logger.debug(f"SkyrimPrefs.ini already exists, skipping: {skyrimprefs_path}")
except Exception as e:
self.logger.warning(f"Could not create SkyrimPrefs.ini stub: {e}")
def _seed_fo4_first_launch_files(self, prefix_user: str, docs_dir_name: str) -> None:
"""
Pre-seed files in the Wine prefix that Fallout 4 needs on first launch.
1. AppData/Local/Fallout4/Plugins.txt - empty anchor file for USVFS.
Same mutex deadlock mechanism as Skyrim SE - confirmed to apply to FO4.
INI stub for CC popup suppression is intentionally omitted until the correct
key name in Fallout4Prefs.ini is confirmed via testing.
"""
appdata_fo4 = os.path.join(prefix_user, "AppData", "Local", docs_dir_name)
plugins_txt = os.path.join(appdata_fo4, "Plugins.txt")
try:
os.makedirs(appdata_fo4, exist_ok=True)
if not os.path.exists(plugins_txt):
open(plugins_txt, 'w').close()
self.logger.info(f"Created Plugins.txt anchor for USVFS: {plugins_txt}")
else:
self.logger.debug(f"Plugins.txt already exists, skipping: {plugins_txt}")
except Exception as e:
self.logger.warning(f"Could not create Plugins.txt anchor: {e}")
def _seed_skyrimvr_first_launch_files(self, prefix_user: str, docs_dir_name: str) -> None:
"""
Pre-seed files in the Wine prefix that Skyrim VR needs on first launch.
1. AppData/Local/Skyrim VR/Plugins.txt - empty anchor file for USVFS.
Same mutex deadlock mechanism as Skyrim SE applies to VR.
2. Documents/My Games/Skyrim VR/SkyrimPrefs.ini - minimal stub with two keys:
- bDownloadCC=1: suppresses the AE/CC download prompt (same engine behaviour
as Skyrim SE; fires before PPR hooks the INI API).
- bLoadVRPlayroom=0: prevents the game loading the Bethesda VR playroom
tutorial on first launch. Without this, SkyrimVR skips the main menu and
drops the user into the playroom, bypassing the modlist's startup sequence.
"""
appdata_vr = os.path.join(prefix_user, "AppData", "Local", docs_dir_name)
plugins_txt = os.path.join(appdata_vr, "Plugins.txt")
try:
os.makedirs(appdata_vr, exist_ok=True)
if not os.path.exists(plugins_txt):
open(plugins_txt, 'w').close()
self.logger.info(f"Created Plugins.txt anchor for USVFS: {plugins_txt}")
else:
self.logger.debug(f"Plugins.txt already exists, skipping: {plugins_txt}")
except Exception as e:
self.logger.warning(f"Could not create Plugins.txt anchor: {e}")
skyrimprefs_path = os.path.join(
prefix_user, "Documents", "My Games", docs_dir_name, "SkyrimPrefs.ini"
)
try:
if not os.path.exists(skyrimprefs_path):
with open(skyrimprefs_path, 'w', encoding='utf-8') as f:
f.write("[General]\nbDownloadCC=1\nbLoadVRPlayroom=0\n")
self.logger.info(f"Created SkyrimPrefs.ini stub for VR first-launch: {skyrimprefs_path}")
else:
self.logger.debug(f"SkyrimPrefs.ini already exists, skipping: {skyrimprefs_path}")
except Exception as e:
self.logger.warning(f"Could not create SkyrimPrefs.ini stub: {e}")
def _seed_fallout4vr_first_launch_files(self, prefix_user: str, docs_dir_name: str) -> None:
"""
Pre-seed files in the Wine prefix that Fallout 4 VR needs on first launch.
1. AppData/Local/Fallout4VR/Plugins.txt - empty anchor file for USVFS.
Same mutex deadlock mechanism as Skyrim SE and FO4 applies to VR.
INI stub is intentionally omitted - the correct key name in Fallout4VRPrefs.ini
has not been confirmed via testing.
"""
appdata_fo4vr = os.path.join(prefix_user, "AppData", "Local", docs_dir_name)
plugins_txt = os.path.join(appdata_fo4vr, "Plugins.txt")
try:
os.makedirs(appdata_fo4vr, exist_ok=True)
if not os.path.exists(plugins_txt):
open(plugins_txt, 'w').close()
self.logger.info(f"Created Plugins.txt anchor for USVFS: {plugins_txt}")
else:
self.logger.debug(f"Plugins.txt already exists, skipping: {plugins_txt}")
except Exception as e:
self.logger.warning(f"Could not create Plugins.txt anchor: {e}")

View File

@@ -64,7 +64,7 @@ class FilesystemSteamMixin:
default_path = Path.home() / ".steam/steam/steamapps/common"
if default_path.is_dir():
logger.warning(f"Using default Steam library path: {default_path}")
logger.info(f"Using default Steam library path: {default_path}")
return default_path
logger.error("No valid Steam library found via vdf or at default location.")

Some files were not shown because too many files have changed in this diff Show More