Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
33b3fbaed2 | ||
|
|
2ff09a1448 | ||
|
|
69fabb32e6 | ||
|
|
6453665620 | ||
|
|
cacbbf1fb1 | ||
|
|
c3551cd269 | ||
|
|
8e4dd06f11 | ||
|
|
e52e1427f6 | ||
|
|
c294431a35 | ||
|
|
7278efd4cd | ||
|
|
b29568f590 | ||
|
|
3556914560 | ||
|
|
411addeea2 | ||
|
|
805718222a | ||
|
|
2eb54b9a36 | ||
|
|
9cc5245db7 | ||
|
|
69738e8e9e | ||
|
|
fdee639734 | ||
|
|
b123f6f509 | ||
|
|
368e1bf5ef | ||
|
|
ebf61f67db | ||
|
|
15b90d823c | ||
|
|
309303f721 | ||
|
|
59e03eb38e | ||
|
|
f278b9a8b5 | ||
|
|
9a10812796 | ||
|
|
61cfda5dac | ||
|
|
9560c1b72a | ||
|
|
5be65c25ac | ||
|
|
053aab04a9 | ||
|
|
f8cdd26d64 | ||
|
|
bc6c0f2e1f |
164
CHANGELOG.md
@@ -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
@@ -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.
|
||||
|
||||
[](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
|
||||
|
||||
---
|
||||
|
||||
[](https://ko-fi.com/D1D8H8WBD)
|
||||
|
||||
**Jackify** - Simplifying Wabbajack modlist installation and configuration on Linux
|
||||
|
After Width: | Height: | Size: 184 KiB |
|
After Width: | Height: | Size: 183 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 187 KiB |
|
After Width: | Height: | Size: 190 KiB |
|
After Width: | Height: | Size: 186 KiB |
BIN
assets/images/wiki/ModlistGuides/Jackify/jackify-main-window.png
Normal file
|
After Width: | Height: | Size: 133 KiB |
|
After Width: | Height: | Size: 174 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 161 KiB |
|
After Width: | Height: | Size: 187 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 143 KiB |
|
After Width: | Height: | Size: 146 KiB |
|
After Width: | Height: | Size: 148 KiB |
|
After Width: | Height: | Size: 174 KiB |
|
After Width: | Height: | Size: 124 KiB |
|
After Width: | Height: | Size: 118 KiB |
|
After Width: | Height: | Size: 170 KiB |
|
After Width: | Height: | Size: 192 KiB |
|
After Width: | Height: | Size: 170 KiB |
|
After Width: | Height: | Size: 634 KiB |
|
After Width: | Height: | Size: 439 KiB |
|
After Width: | Height: | Size: 957 KiB |
BIN
assets/images/wiki/ModlistGuides/Wabbajack/wj-first-launch.png
Normal file
|
After Width: | Height: | Size: 65 KiB |
|
After Width: | Height: | Size: 678 KiB |
|
After Width: | Height: | Size: 685 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 344 KiB |
BIN
assets/images/wiki/ModlistGuides/Wabbajack/wj-settings-login.png
Normal file
|
After Width: | Height: | Size: 241 KiB |
|
After Width: | Height: | Size: 118 KiB |
BIN
assets/images/wiki/UserGuide/AdditionalTools/protonplus-main.png
Normal file
|
After Width: | Height: | Size: 150 KiB |
|
After Width: | Height: | Size: 60 KiB |
BIN
assets/images/wiki/UserGuide/AdditionalTools/protonupqt-main.png
Normal file
|
After Width: | Height: | Size: 100 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 165 KiB |
|
After Width: | Height: | Size: 129 KiB |
|
After Width: | Height: | Size: 124 KiB |
|
After Width: | Height: | Size: 138 KiB |
|
After Width: | Height: | Size: 180 KiB |
|
After Width: | Height: | Size: 144 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 77 KiB |
BIN
assets/images/wiki/UserGuide/shared/Jackify_Github_Banner.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 117 KiB |
|
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 93 KiB After Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 130 KiB After Width: | Height: | Size: 130 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 104 KiB |
|
After Width: | Height: | Size: 44 KiB |
BIN
assets/images/wiki/UserGuide/shared/Shared/mo2-run-button.png
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
|
After Width: | Height: | Size: 405 KiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 138 KiB After Width: | Height: | Size: 138 KiB |
|
Before Width: | Height: | Size: 154 KiB After Width: | Height: | Size: 154 KiB |
|
Before Width: | Height: | Size: 149 KiB After Width: | Height: | Size: 149 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 130 KiB After Width: | Height: | Size: 130 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 104 KiB |
BIN
assets/images/wiki/UserGuide/shared/mo2-run-button.png
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
|
After Width: | Height: | Size: 405 KiB |
|
After Width: | Height: | Size: 1.4 MiB |
@@ -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"
|
||||
|
||||
@@ -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}")
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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.")
|
||||
|
||||