Compare commits
51 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
12294d3186 | ||
|
|
b55e1cf768 | ||
|
|
8e49602714 | ||
|
|
98a9a4c7c6 | ||
|
|
286d51e6a1 | ||
|
|
53af9f26a2 | ||
|
|
9000b1e080 | ||
|
|
02f3d71a82 | ||
|
|
29e1800074 | ||
|
|
9b5310c2f9 | ||
|
|
0d84d2f2fe | ||
|
|
2511c9334c | ||
|
|
5869a896a8 | ||
|
|
99fb369d5e | ||
|
|
a813236e51 | ||
|
|
a7ed4b2a1e | ||
|
|
523681a254 | ||
|
|
abfca5268f | ||
|
|
4de5c7f55d | ||
|
|
9c52c0434b | ||
|
|
e3dc62fdac | ||
|
|
ce969eba1b | ||
|
|
fe14e4ecfb | ||
|
|
9680814bbb | ||
|
|
91ac08afb2 | ||
|
|
06bd94d119 |
539
CHANGELOG.md
@@ -1,5 +1,525 @@
|
||||
# Jackify Changelog
|
||||
|
||||
## 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
|
||||
|
||||
### Technical Improvements
|
||||
- **Code Architecture**: Refactored 13 large files (1000-5000 lines each) into 50+ focused modules using mixin pattern. All main files now under 600 lines.
|
||||
|
||||
### Bug Fixes
|
||||
- **Configure New Modlist GUI**: Fixed window not shrinking when Show Details unchecked
|
||||
- **CLI Wabbajack Installer**: Added missing installation command to CLI menu
|
||||
- **Wabbajack Installer**: Fixed installation to non-primary disk
|
||||
|
||||
### Improvements
|
||||
- **Wabbajack Install - Honour Install Proton**: Wabbajack installer now uses the user's selected Install Proton from Settings (same as modlist install/configure). Previously hardcoded to Proton Experimental. Fallback to Proton Experimental when no selection or path invalid.
|
||||
- **STEAM_COMPAT_MOUNTS (Issue #155)**: Launch options now include mountpoints for both the modlist install path and the download path when known, so MO2 can access game and downloads on different drives. Uses new mountpoint helper and passes install_dir/download_dir through the Install a Modlist workflow.
|
||||
- **MO2 download_directory (Issue #154)**: When configuring after Install a Modlist, Jackify now sets `download_directory` in ModOrganizer.ini to the correct Wine path (Z: or D: on SD card) so MO2 finds the download folder. Configure New and Configure Existing continue to leave or blank the key as before.
|
||||
- **Winetricks / Protontricks**: For Flatpak Steam, use protontricks only. Winetricks alone struggles with the flatpak sandbox.
|
||||
- **Wine Component Animation**: Added pulser animation for individual wine component installation progress in Configure Existing and Install Modlist workflows
|
||||
- **Wabbajack Installer Log Rotation**: Added log rotation for Wabbajack installer workflow logs
|
||||
|
||||
---
|
||||
|
||||
## v0.2.2.2 - ModOrganizer.ini Path Fixes for SD Card Installations
|
||||
**Release Date:** 2026-01-28
|
||||
|
||||
### Bug Fixes
|
||||
- **ModOrganizer.ini Path Mangling**: Fixed incorrect drive letter assignment when modlist is on SD card but vanilla game is on internal storage. Now uses gamePath drive letter as source of truth for vanilla game paths.
|
||||
- **Proton Config Name Mismatch (Issues #150, #151)**: Fixed incorrect Proton names written to Steam config.vdf CompatToolMapping. Naive string conversion produced wrong names (e.g., `proton_9.0_(beta)` instead of `proton_9`). Now resolves correct internal names from `compatibilitytool.vdf` (third-party) or App ID mapping (Valve Proton). CachyOS and other community Proton builds in `compatibilitytools.d/` are now detected and selectable.
|
||||
- **Removed Lorerim/Lost Legacy Proton Override**: No longer forces Proton 9 for specific modlists. ENB compatibility warnings are handled by the success dialog instead.
|
||||
|
||||
### Engine Updates
|
||||
- **jackify-engine 0.4.7**: Fixed incorrect quoting/escaping of MO2 `customExecutables` by writing clean, unquoted Proton `Z:\...` paths in `ModOrganizer.ini`. This eliminates engine-side quote corruption that previously triggered SD card path mangling issues.
|
||||
|
||||
### Improvements
|
||||
- **Improved Wine Component install debug log output**: Will now print the full command being used for winetricks and protontricks when debug mode is enabled, making it easier to reproduce issues manually.
|
||||
|
||||
---
|
||||
|
||||
## v0.2.2.1 - TTW Installer Pinning and Configure New Modlist CLI Fix
|
||||
**Release Date:** 2026-01-24
|
||||
|
||||
### Bug Fixes
|
||||
- **Configure New Modlist CLI**: Fixed manual Proton setup prompts appearing in CLI. Now uses automated prefix workflow like the install command.
|
||||
- **TTW_Linux_Installer Version Pinning**: Pinned to v0.0.7. Will re-introduce latest version following more testing.
|
||||
|
||||
---
|
||||
|
||||
## v0.2.2 - VNV Automation and First-Launch Improvements
|
||||
**Release Date:** 2026-01-21
|
||||
|
||||
### Major Features
|
||||
- **Viva New Vegas Post-Install Automation (experimental)**: Full automated workflow for the Viva New Vegas modlist. Handles root files copying, 4GB patcher, and BSA decompression as per the VNV install guide. This is an initial pass at automating this, so considered experimental.
|
||||
- **Game Directory Pre-Creation**: Automatically creates My Documents/My Games and AppData/Local directories for some. Prevents some first-launch failures where games can't initialize under Proton. Supports Skyrim SE, FNV, FO4, Oblivion, Oblivion Remastered, Enderal, and Starfield so far.
|
||||
|
||||
### Bug Fixes
|
||||
- **Configure Existing Modlist**: Fixed AttributeError when VNV automation check runs after configuration completes
|
||||
- **Enderal Directory Creation**: Fixed bug where Enderal My Documents directory was created for all modlists instead of only Enderal
|
||||
|
||||
### Improvements
|
||||
- **Winetricks Bundling**: Implemented Wine wrapper scripts that replicate protontricks' environment setup for improved reliability
|
||||
|
||||
---
|
||||
|
||||
## v0.2.1.1 - Bug Fixes and Improvements
|
||||
**Release Date:** 2026-01-15
|
||||
|
||||
### Critical Bug Fixes
|
||||
- **AppImage Crash on Steam Deck**: Fixed `NameError: name 'Tuple' is not defined` that prevented AppImage from launching on Steam Deck. Added missing `Tuple` import to `progress_models.py`
|
||||
|
||||
### Bug Fixes
|
||||
- **Menu Routing**: Fixed "Configure Existing Modlist (In Steam)" opening wrong section (was routing to Wabbajack Installer instead of Configure Existing screen)
|
||||
- **TTW Install Dialogue**: Fixed incorrect account reference (changed "mod.db" to "ModPub" to match actual download source)
|
||||
- **Duplicate Method**: Removed duplicate `_handle_missing_downloader_error` method in winetricks handler
|
||||
- **Issue #142**: Removed sudo execution from modlist configuration - now auto-fixes permissions when possible, provides manual instructions only when sudo required
|
||||
- **Issue #133**: Updated VDF library to 4.0 for improved Steam file format compatibility (protontricks 1.13.1+ support)
|
||||
|
||||
### Features
|
||||
- **Wine Component Error Handling**: Enhanced error messages for missing downloaders with platform-specific installation instructions (SteamOS/Steam Deck vs other distros)
|
||||
|
||||
### Dependencies
|
||||
- **VDF Library**: Updated from PyPI vdf 3.4 to actively maintained solsticegamestudios/vdf 4.0 (used by Gentoo)
|
||||
- **Winetricks**: Removed bundled downloaders that caused segfaults on some systems - now uses system-provided downloaders (aria2c/wget/curl)
|
||||
|
||||
---
|
||||
|
||||
## v0.2.1 - Wabbajack Installer and ENB Support
|
||||
**Release Date:** 2025-01-12
|
||||
Y
|
||||
### Major Features
|
||||
- **Automated Wabbajack Installation**: While I work on Non-Premium support, there is still a call for Wabbajack via Proton. The existing legacy bash script has been proving troublesome for some users, so I've added this as a new feature within Jackify. My aim is still to not need this in future, once Jackify can cover Non-Premium accounts.
|
||||
- **ENB Detection and Configuration**: Automatic detection and configuration of `enblocal.ini` with `LinuxVersion=true` for all supported games
|
||||
- **ENB Proton Warning**: Dedicated dialog with Proton version recommendations when ENB is detected
|
||||
|
||||
### Critical Bug Fixes
|
||||
- **OAuth Token Stale State**: Re-check authentication before engine launch to prevent stale token errors after revocation
|
||||
- **FNV SD Card Registry**: Fixed launcher not recognizing game on SD cards (uses `D:` drive for SD, `Z:` for internal)
|
||||
- **CLI FILE_PROGRESS Spam**: Filter verbose output to preserve single-line progress updates
|
||||
- **Steam Double Restart**: Removed legacy code causing double restart during configuration
|
||||
- **TTW Installer lz4**: Fixed bundled lz4 detection by setting correct working directory
|
||||
|
||||
### Improvements
|
||||
- **Winetricks Bundling**: Bundled critical dependencies (wget, sha256sum, unzip, 7z) for improved reliability
|
||||
- **UI/UX**: Removed per-file download speeds to match Wabbajack upstream
|
||||
- **Code Cleanup**: Removed PyInstaller references, use AppImage detection only
|
||||
- **Wabbajack Installer UI**: Removed unused Process Monitor tab, improved Activity window with detailed step information
|
||||
- **Steam AppID Overflow Fix**: Changed AppID handling to string type to prevent overflow errors with large Steam AppIDs
|
||||
|
||||
---
|
||||
|
||||
## v0.2.0.10 - Registry & Hashing Fixes
|
||||
**Release Date:** 2025-01-04
|
||||
|
||||
### Engine Updates
|
||||
- **jackify-engine 0.4.5**: Fixed archive extraction with backslashes (including pattern matching), data directory path configuration, and removed post-download .wabbajack hash validation. Engine now auto-refreshes OAuth tokens during long installations via `NEXUS_OAUTH_INFO` environment variable.
|
||||
|
||||
### Critical Bug Fixes
|
||||
- **InstallationThread Crash**: Fixed crash during installation with error "'InstallationThread' object has no attribute 'auth_service'". Premium detection diagnostics code assumed auth_service existed but it was never passed to the thread. Affects all users when Premium detection (including false positives) is triggered.
|
||||
- **Install Start Hang**: Fixed missing `oauth_info` parameter that prevented modlist installs from starting (hung at "Starting modlist installation...")
|
||||
- **OAuth Token Auto-Refresh**: Fixed OAuth tokens expiring during long modlist installations. Jackify now refreshes tokens with 15-minute buffer before passing to engine. Engine receives full OAuth state via `NEXUS_OAUTH_INFO` environment variable, enabling automatic token refresh during multi-hour downloads. Fixes "Token has expired" errors that occurred 60 minutes into installations.
|
||||
- **ShowDotFiles Registry Format**: Fixed Wine registry format bug causing hidden files to remain hidden in prefixes. Python string escaping issue wrote single backslash instead of double backslash in `[Software\\Wine]` section header. Added auto-detection and fix for broken format from curated registry files.
|
||||
- **Dotnet4 Registry Fixes**: Confirmed universal dotnet4.x registry fixes (`*mscoree=native` and `OnlyUseLatestCLR=1`) are applied in all three workflows (Install, Configure New, Configure Existing) across both CLI and GUI interfaces
|
||||
- **Proton Path Configuration**: Fixed `proton_path` writing invalid "auto" string to config.json - now uses `null` instead, preventing jackify-engine from receiving invalid paths
|
||||
|
||||
### Improvements
|
||||
- **Wine Binary Detection**: Enhanced detection with recursive fallback search within Proton directory when expected paths don't exist (handles different Proton version structures)
|
||||
- Added Jackify version logging at workflow start
|
||||
- Fixed GUI log file rotation to only run in debug mode
|
||||
|
||||
---
|
||||
|
||||
## v0.2.0.9 - Critical Configuration Fixes
|
||||
**Release Date:** 2025-12-31
|
||||
|
||||
### Bug Fixes
|
||||
- Fixed AppID conversion bug causing Configure Existing failures
|
||||
- Fixed missing MessageService import crash in Configure Existing
|
||||
- Fixed RecursionError in config_handler.py logger
|
||||
- Fixed winetricks automatic fallback to protontricks (was silently failing)
|
||||
|
||||
### Improvements
|
||||
- Added detailed progress indicators for configuration workflows
|
||||
- Fixed progress bar completion showing 100% instead of 95%
|
||||
- Removed debug logging noise from file progress widget
|
||||
- Enhanced Premium detection diagnostics for Issue #111
|
||||
- Flatpak protontricks now auto-granted cache access for faster subsequent installs
|
||||
|
||||
---
|
||||
|
||||
## v0.2.0.8 - Bug Fixes and Improvements
|
||||
**Release Date:** 2025-12-29
|
||||
|
||||
### Bug Fixes
|
||||
- Fixed Configure New/Existing/TTW screens missing Activity tab and progress updates
|
||||
- Fixed cancel/back buttons crashing in Configure workflows
|
||||
|
||||
### Improvements
|
||||
- Install directory now auto-appends modlist name when selected from gallery
|
||||
|
||||
### Known Issues
|
||||
- Mod filter temporarily disabled in gallery due to technical issue (tag and game filters still work)
|
||||
|
||||
---
|
||||
|
||||
## v0.2.0.7 - Critical Auth Fix
|
||||
**Release Date:** 2025-12-28
|
||||
|
||||
### Critical Bug Fixes
|
||||
- **OAuth Token Loss**: Fixed version comparison bug that was deleting OAuth tokens every time settings were saved (affects users on v0.2.0.4+)
|
||||
- Fixed internal import paths for improved stability
|
||||
|
||||
---
|
||||
|
||||
## v0.2.0.6 - Premium Detection and Engine Update
|
||||
**Release Date:** 2025-12-28
|
||||
|
||||
**IMPORTANT:** If you are on v0.2.0.5, automatic updates will not work. You must manually download and install v0.2.0.6.
|
||||
|
||||
### Engine Updates
|
||||
- **jackify-engine 0.4.4**: Latest engine version with improvements
|
||||
|
||||
### Critical Bug Fixes
|
||||
- **Auto-Update System**: Fixed broken update dialog import that prevented automatic updates
|
||||
- **Premium Detection**: Fixed false Premium errors caused by overly-broad detection pattern triggering on jackify-engine 0.4.3's userinfo JSON output
|
||||
- **Custom Data Directory**: Fixed AppImage always creating ~/Jackify on startup, even when user configured a custom jackify_data_dir
|
||||
- **Proton Auto-Selection**: Fixed auto-selection writing invalid "auto" string to config on detection failure
|
||||
|
||||
### Quality Improvements
|
||||
- Added pre-build import validator to prevent broken imports from reaching production
|
||||
|
||||
---
|
||||
|
||||
## v0.2.0.5 - Emergency OAuth Fix
|
||||
**Release Date:** 2025-12-24
|
||||
|
||||
### Critical Bug Fixes
|
||||
- **OAuth Authentication**: Fixed regression in v0.2.0.4 that prevented OAuth token encryption/decryption, breaking Nexus authentication for users
|
||||
|
||||
---
|
||||
|
||||
## v0.2.0.4 - Bugfixes & Improvements
|
||||
**Release Date:** 2025-12-23
|
||||
|
||||
### Engine Updates
|
||||
- **jackify-engine 0.4.3**: Fixed case sensitivity issues, archive extraction crashes, and improved error messages
|
||||
|
||||
### Bug Fixes
|
||||
- Fixed modlist gallery metadata showing outdated versions (now always fetches fresh data)
|
||||
- Fixed hardcoded ~/Jackify paths preventing custom data directory settings
|
||||
- Fixed update check blocking GUI startup
|
||||
- Improved Steam restart reliability (3-minute timeout, better error handling)
|
||||
- Fixed Protontricks Flatpak installation on Steam Deck
|
||||
|
||||
### Backend Changes
|
||||
- GPU texture conversion now always enabled (config setting deprecated)
|
||||
|
||||
### UI Improvements
|
||||
- Redesigned modlist detail view to show more of hero image
|
||||
- Improved gallery loading with animated feedback and faster initial load
|
||||
|
||||
---
|
||||
|
||||
## v0.2.0.3 - Engine Bugfix & Settings Cleanup
|
||||
**Release Date:** 2025-12-21
|
||||
|
||||
### Engine Updates
|
||||
- **jackify-engine 0.4.3**: Bugfix release
|
||||
|
||||
### UI Improvements
|
||||
- **Settings Dialog**: Removed GPU disable toggle - GPU usage is now always enabled (the disable option was non-functional)
|
||||
|
||||
---
|
||||
|
||||
## v0.2.0.2 - Emergency Engine Bugfix
|
||||
**Release Date:** 2025-12-18
|
||||
|
||||
### Engine Updates
|
||||
- **jackify-engine 0.4.2**: Fixed OOM issue with jackify-engine 0.4.1 due to array size
|
||||
|
||||
---
|
||||
|
||||
## v0.2.0.1 - Critical Bugfix Release
|
||||
**Release Date:** 2025-12-15
|
||||
|
||||
### Critical Bug Fixes
|
||||
- **Directory Safety Validation**: Fixed data loss bug where directories with only a `downloads/` folder were incorrectly identified as valid modlist directories
|
||||
- **Flatpak Steam Restart**: Fixed Steam restart failures on Ubuntu/PopOS by removing incompatible `-foreground` flag and increasing startup wait
|
||||
|
||||
### Bug Fixes
|
||||
- **External Links**: Fixed Ko-fi, GitHub, and Nexus links not opening on some distros using xdg-open with clean environment
|
||||
- **TTW Console Output**: Filtered standalone "OK"/"DONE" noise messages from TTW installation console
|
||||
- **Activity Window**: Fixed progress display updates in TTW Installer and other workflows
|
||||
- **Wine Component Installation**: Added status feedback during component installation showing component list
|
||||
- **Progress Parser**: Added defensive checks to prevent segfaults from malformed engine output
|
||||
- **Progress Parser Speed Info**: Fixed 'OperationType' object has no attribute 'lower' error by converting enum to string value when extracting speed info from timestamp status patterns
|
||||
|
||||
### Improvements
|
||||
- **Default Wine Components**: Added dxvk to default component list for better graphics compatibility
|
||||
- **TTW Installer UI**: Show version numbers in status displays
|
||||
|
||||
### Engine Updates
|
||||
- **jackify-engine 0.4.1**: Download reliability fixes, BSA case sensitivity handling, external drive I/O limiting, GPU detection caching, and texture processing performance improvements
|
||||
|
||||
---
|
||||
|
||||
## v0.2.0 - Modlist Gallery, OAuth Authentication & Performance Improvements
|
||||
**Release Date:** 2025-12-06
|
||||
|
||||
### Major Features
|
||||
|
||||
#### Modlist Selection Gallery
|
||||
Complete overhaul of modlist selection (First pass):
|
||||
|
||||
**Core Features:**
|
||||
- Card-based Modlist Selection browser with modlist images, titles, authors and metadata
|
||||
- Game-specific filtering automatically applied based on selected game type
|
||||
- Details per card: download/install/total sizes, tags, version, badges
|
||||
- Async image loading from GitHub with local 7-day caching
|
||||
- Detail view with full descriptions, banner images, and external links
|
||||
- Selected modlist automatically populates Install Modlist workflow
|
||||
|
||||
**Search and Filtering:**
|
||||
- Text search across modlist names and descriptions
|
||||
- Multi-select tag filtering with normalized tags
|
||||
- Show Official Only, Show NSFW, Hide Unavailable toggles
|
||||
- Mod search capability - find modlists containing specific Nexus mods
|
||||
- Randomised card ordering
|
||||
|
||||
**Performance:**
|
||||
- Gallery images loading from cache
|
||||
- Background metadata and image preloading when Install Modlist screen opens
|
||||
- Efficient rendering - cards created once, filters toggle visibility
|
||||
- Non-blocking UI with concurrent image downloads
|
||||
|
||||
**Steam Deck Optimized:**
|
||||
- Dynamic card sizing (e.g 250x270 on Steam Deck, larger on desktop)
|
||||
- Responsive grid layout (up to 4 columns on large screens, 3 on Steam Deck)
|
||||
- Optimized spacing and padding for 1280x800 displays
|
||||
|
||||
#### OAuth 2.0 Authentication
|
||||
Modern authentication for Nexus Mods with secure token management:
|
||||
|
||||
- One-click browser-based authorization with PKCE security
|
||||
- Automatic token refresh with encrypted storage
|
||||
- Authorisation status indicator on Install Modlist screen
|
||||
- Works in both GUI and CLI workflows
|
||||
|
||||
#### Compact Mode UI Redesign
|
||||
Streamlined interface with dynamic window management:
|
||||
|
||||
- Default compact mode with optional Details view
|
||||
- Activity window tab (default), across all workflow screens
|
||||
- Process Monitor tab still available
|
||||
- Show Details toggle for console output when needed
|
||||
|
||||
### Critical Fixes
|
||||
|
||||
### Replaced TTW Installer
|
||||
- Replaced the previous TTW Installer due to complexities with its config file
|
||||
|
||||
#### GPU Texture Conversion (jackify-engine 0.4.0)
|
||||
- Fixed GPU not being used for BC7/BC6H texture conversions
|
||||
- Previous versions fell back to CPU-only despite GPU availability
|
||||
- Added GPU toggle in Settings (enabled by default)
|
||||
|
||||
#### Winetricks Compatibility & Protontricks
|
||||
- Fixed bundled winetricks path incompatibility
|
||||
- Hopefully fixed winetricks in cases where it failed to download components
|
||||
- For now, Jackify still defaults to bundled winetricks (Protontricks toggle in settings)
|
||||
|
||||
#### Steam Restart Reliability
|
||||
- Enhanced Steam Restart so that is now hopefully works more reliably on all distros
|
||||
- Fixed Flatpak detection blocking normal Steam start methods
|
||||
|
||||
### Technical Improvements
|
||||
|
||||
- Proton version usage clarified: Install Proton for installation/texture processing, Game Proton for shortcuts
|
||||
- Centralised Steam detection in SystemInfo
|
||||
- ConfigHandler refactored to always read fresh from disk
|
||||
- Removed obsolete dotnet4.x code
|
||||
- Enhanced Flatpak Steam compatdata detection with proper VDF parsing
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- TTW installation UI performance (batched output processing, non-blocking operations)
|
||||
- Activity window animations (removed custom timers, Qt native rendering)
|
||||
- Timer reset when returning from TTW screen
|
||||
- Fixed bandwidth limit KB/s to bytes conversion
|
||||
- Fixed AttributeError in AutomatedPrefixService.restart_steam()
|
||||
|
||||
### Engine Updates
|
||||
- jackify-engine 0.4.0 with GPU texture conversion fixes and refactored file progress reporting
|
||||
|
||||
---
|
||||
|
||||
## v0.1.7.1 - Wine Component Verification & Flatpak Steam Fixes
|
||||
**Release Date:** November 11, 2025
|
||||
|
||||
### Critical Bug Fixes
|
||||
- **FIXED: Wine Component Installation Verification** - Jackify now verifies components are actually installed before reporting success
|
||||
|
||||
### Bug Fixes
|
||||
- **Steam Deck SD Card Paths**: Fixed ModOrganizer.ini path corruption on SD card installs using regex-based stripping
|
||||
- **Flatpak Steam Detection**: Fixed libraryfolders.vdf path detection for Flatpak Steam installations
|
||||
- **Flatpak Steam Restart**: Steam restart service now properly detects and controls Flatpak Steam
|
||||
- **Path Manipulation**: Fixed path corruption in Configure Existing/New Modlist (paths with spaces)
|
||||
|
||||
### Improvements
|
||||
- Added network diagnostics before winetricks fallback to protontricks
|
||||
- Enhanced component installation logging with verification status
|
||||
- Added GE-Proton 10-14 recommendation to success message (ENB compatibility note for Valve's Proton 10)
|
||||
|
||||
### Engine Updates
|
||||
- **jackify-engine 0.3.18**: Archive extraction fixes for Windows symlinks, bandwidth limiting fix, improved error messages
|
||||
|
||||
---
|
||||
|
||||
## v0.1.7 - TTW Automation & Bug Fixes
|
||||
**Release Date:** November 1, 2025
|
||||
|
||||
### Major Features
|
||||
- **TTW (Tale of Two Wastelands) Installation and Automation**
|
||||
laf - TTW Installation function using Hoolamike application - https://github.com/Niedzwiedzw/hoolamike
|
||||
- Automated workflow for TTW installation and integration into FNV modlists, where possible
|
||||
- Automatic detection of TTW-compatible modlists
|
||||
- User prompt after modlist installation with option to install TTW
|
||||
- Automated integration: file copying, load order updates, modlist.txt updates
|
||||
- Available in both CLI and GUI workflows
|
||||
|
||||
### Bug Fixes
|
||||
- **Registry UTF-8 Decode Error**: Fixed crash during dotnet4.x installation when Wine outputs binary data
|
||||
- **Python 3.10 Compatibility**: Fixed startup crash on Python 3.10 systems
|
||||
- **TTW Steam Deck Layout**: Fixed window sizing issues on Steam Deck when entering/exiting TTW screen
|
||||
- **TTW Integration Status**: Added visible status banner updates during modlist integration for collapsed mode
|
||||
- **TTW Accidental Input Protection**: Added 3-second countdown to TTW installation prompt to prevent accidental dismissal
|
||||
- **Settings Persistence**: Settings changes now persist correctly across workflows
|
||||
- **Steam Deck Keyboard Input**: Fixed keyboard input failure on Steam Deck
|
||||
- **Application Close Crash**: Fixed crash when closing application on Steam Deck
|
||||
- **Winetricks Diagnostics**: Enhanced error detection with automatic fallback
|
||||
|
||||
---
|
||||
|
||||
## v0.1.6.6 - AppImage Bundling Fix
|
||||
**Release Date:** October 29, 2025
|
||||
|
||||
### Bug Fixes
|
||||
- **Fixed AppImage bundling issue** causing legacy code to be retained in rare circumstances
|
||||
|
||||
---
|
||||
|
||||
## v0.1.6.5 - Steam Deck SD Card Path Fix
|
||||
**Release Date:** October 27, 2025
|
||||
|
||||
### Bug Fixes
|
||||
- **Fixed Steam Deck SD card path manipulation** when jackify-engine installed
|
||||
- **Fixed Ubuntu Qt platform plugin errors** by bundling XCB libraries
|
||||
- **Added Flatpak GE-Proton detection** and protontricks installation choices
|
||||
- **Extended Steam Deck SD card timeouts** for slower I/O operations
|
||||
|
||||
---
|
||||
|
||||
## v0.1.6.4 - Flatpak Steam Detection Hotfix
|
||||
**Release Date:** October 24, 2025
|
||||
|
||||
@@ -413,6 +933,23 @@
|
||||
- **Clean Architecture**: Removed obsolete service imports, initializations, and cleanup methods
|
||||
- **Code Quality**: Eliminated "tombstone comments" and unused service references
|
||||
|
||||
### Deferred Features (Available in Future Release)
|
||||
|
||||
#### OAuth 2.0 Authentication for Nexus Mods
|
||||
**Status:** Fully implemented but disabled pending Nexus Mods approval
|
||||
|
||||
The OAuth 2.0 authentication system has been fully developed and tested, but is temporarily disabled in v0.1.8 as we await approval from Nexus Mods for our OAuth application. The backend code remains intact and will be re-enabled immediately upon approval.
|
||||
|
||||
**Features (ready for deployment):**
|
||||
- **Secure OAuth 2.0 + PKCE Flow**: Modern authentication to replace API key dependency
|
||||
- **Encrypted Token Storage**: Tokens stored using Fernet encryption with automatic refresh
|
||||
- **GUI Integration**: Clean status display on Install Modlist screen with authorize/revoke functionality
|
||||
- **CLI Integration**: OAuth menu in Additional Tasks for command-line users
|
||||
- **API Key Fallback**: Optional legacy API key support (configurable in Settings)
|
||||
- **Unified Auth Service**: Single authentication layer supporting both OAuth and API key methods
|
||||
|
||||
**Current Limitation:** Awaiting Nexus approval for `jackify://oauth/callback` custom URI. Once approved, OAuth will be enabled as the primary authentication method with API key as optional fallback.
|
||||
|
||||
### Technical Details
|
||||
- **Single Shortcut Creation Path**: All workflows now use `run_working_workflow()` → `create_shortcut_with_native_service()`
|
||||
- **Service Layer Cleanup**: Removed dual codepath architecture in favor of proven automated workflows
|
||||
@@ -764,4 +1301,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.
|
||||
|
||||
168
README.md
@@ -2,159 +2,117 @@
|
||||
|
||||
<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 will work)
|
||||
- Steam installed and configured
|
||||
- **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 automated downloads)
|
||||
- Non-Premium accounts are supported, but some downloads may require 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**
|
||||
- **Ubuntu/Debian-based distros only** (Ubuntu, Kubuntu, Linux Mint, Pop!_OS, Zorin OS, elementary OS, and others): Qt platform plugin library
|
||||
- `sudo apt install libxcb-cursor-dev`
|
||||
|
||||
- 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
|
||||
## Installation Quick Start
|
||||
|
||||
### Installation
|
||||
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).
|
||||
|
||||
### Quick Start
|
||||
|
||||
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
|
||||
|
||||
**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.1.6.4"
|
||||
__version__ = "0.5.0.2"
|
||||
|
||||
727
jackify/backend/core/modlist_operations_configuration_cli.py
Normal file
@@ -0,0 +1,727 @@
|
||||
"""CLI configuration phase methods for ModlistInstallCLI (Mixin)."""
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from ..handlers.ui_colors import (
|
||||
COLOR_PROMPT,
|
||||
COLOR_RESET,
|
||||
COLOR_INFO,
|
||||
COLOR_ERROR,
|
||||
COLOR_SUCCESS,
|
||||
COLOR_WARNING,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ModlistOperationsConfigurationCLIMixin:
|
||||
"""Mixin providing CLI configuration phase methods."""
|
||||
|
||||
def configuration_phase(self):
|
||||
"""
|
||||
Run the configuration phase: execute the Linux-native Jackify Install Engine.
|
||||
"""
|
||||
from .modlist_operations import get_jackify_engine_path
|
||||
|
||||
print(f"\n{COLOR_PROMPT}--- Configuration Phase: Installing Modlist ---{COLOR_RESET}")
|
||||
start_time = time.time()
|
||||
|
||||
from jackify.shared.paths import get_jackify_logs_dir
|
||||
log_dir = get_jackify_logs_dir()
|
||||
log_dir.mkdir(parents=True, exist_ok=True)
|
||||
workflow_log_path = log_dir / "Modlist_Install_workflow.log"
|
||||
max_logs = 3
|
||||
max_size = 1024 * 1024
|
||||
if workflow_log_path.exists() and workflow_log_path.stat().st_size > max_size:
|
||||
for i in range(max_logs, 0, -1):
|
||||
prev = log_dir / f"Modlist_Install_workflow.log.{i-1}" if i > 1 else workflow_log_path
|
||||
dest = log_dir / f"Modlist_Install_workflow.log.{i}"
|
||||
if prev.exists():
|
||||
if dest.exists():
|
||||
dest.unlink()
|
||||
prev.rename(dest)
|
||||
workflow_log = open(workflow_log_path, 'a')
|
||||
class TeeStdout:
|
||||
def __init__(self, *files):
|
||||
self.files = files
|
||||
def write(self, data):
|
||||
for f in self.files:
|
||||
f.write(data)
|
||||
f.flush()
|
||||
def flush(self):
|
||||
for f in self.files:
|
||||
f.flush()
|
||||
orig_stdout, orig_stderr = sys.stdout, sys.stderr
|
||||
sys.stdout = TeeStdout(sys.stdout, workflow_log)
|
||||
sys.stderr = TeeStdout(sys.stderr, workflow_log)
|
||||
try:
|
||||
install_dir_context = self.context['install_dir']
|
||||
if isinstance(install_dir_context, tuple):
|
||||
actual_install_path = Path(install_dir_context[0])
|
||||
if install_dir_context[1]:
|
||||
self.logger.info(f"Creating install directory as it was marked for creation: {actual_install_path}")
|
||||
actual_install_path.mkdir(parents=True, exist_ok=True)
|
||||
else:
|
||||
actual_install_path = Path(install_dir_context)
|
||||
install_dir_str = str(actual_install_path)
|
||||
self.logger.debug(f"Processed install directory for engine: {install_dir_str}")
|
||||
|
||||
download_dir_context = self.context['download_dir']
|
||||
if isinstance(download_dir_context, tuple):
|
||||
actual_download_path = Path(download_dir_context[0])
|
||||
if download_dir_context[1]:
|
||||
self.logger.info(f"Creating download directory as it was marked for creation: {actual_download_path}")
|
||||
actual_download_path.mkdir(parents=True, exist_ok=True)
|
||||
else:
|
||||
actual_download_path = Path(download_dir_context)
|
||||
download_dir_str = str(actual_download_path)
|
||||
self.logger.debug(f"Processed download directory for engine: {download_dir_str}")
|
||||
|
||||
modlist_arg = self.context.get('modlist_value') or self.context.get('machineid')
|
||||
machineid = self.context.get('machineid')
|
||||
|
||||
from jackify.backend.services.nexus_auth_service import NexusAuthService
|
||||
auth_service = NexusAuthService()
|
||||
current_api_key, current_oauth_info = auth_service.get_auth_for_engine()
|
||||
|
||||
api_key = current_api_key or self.context.get('nexus_api_key')
|
||||
oauth_info = current_oauth_info or self.context.get('nexus_oauth_info')
|
||||
|
||||
engine_path = get_jackify_engine_path()
|
||||
engine_dir = os.path.dirname(engine_path)
|
||||
if not os.path.isfile(engine_path) or not os.access(engine_path, os.X_OK):
|
||||
print(f"{COLOR_ERROR}Jackify Install Engine not found or not executable at: {engine_path}{COLOR_RESET}")
|
||||
return
|
||||
|
||||
if os.environ.get('JACKIFY_GUI_MODE') == '1':
|
||||
if not self.context.get('modlist_source'):
|
||||
self.context['modlist_source'] = 'identifier'
|
||||
if not self.context.get('modlist_value'):
|
||||
self.logger.error("modlist_value is missing in context for GUI workflow!")
|
||||
return
|
||||
|
||||
cmd = [engine_path, 'install', '--show-file-progress']
|
||||
modlist_value = self.context.get('modlist_value')
|
||||
if modlist_value and modlist_value.endswith('.wabbajack') and os.path.isfile(modlist_value):
|
||||
cmd += ['-w', modlist_value]
|
||||
elif modlist_value:
|
||||
cmd += ['-m', modlist_value]
|
||||
elif self.context.get('machineid'):
|
||||
cmd += ['-m', self.context['machineid']]
|
||||
cmd += ['-o', install_dir_str, '-d', download_dir_str]
|
||||
|
||||
from jackify.backend.handlers.config_handler import ConfigHandler
|
||||
config_handler = ConfigHandler()
|
||||
debug_mode = config_handler.get('debug_mode', False)
|
||||
if debug_mode:
|
||||
cmd.append('--debug')
|
||||
self.logger.info("Adding --debug flag to jackify-engine")
|
||||
|
||||
original_env_values = {
|
||||
'NEXUS_API_KEY': os.environ.get('NEXUS_API_KEY'),
|
||||
'NEXUS_OAUTH_INFO': os.environ.get('NEXUS_OAUTH_INFO'),
|
||||
'DOTNET_SYSTEM_GLOBALIZATION_INVARIANT': os.environ.get('DOTNET_SYSTEM_GLOBALIZATION_INVARIANT')
|
||||
}
|
||||
|
||||
try:
|
||||
if oauth_info:
|
||||
os.environ['NEXUS_OAUTH_INFO'] = oauth_info
|
||||
from jackify.backend.services.nexus_oauth_service import NexusOAuthService
|
||||
os.environ['NEXUS_OAUTH_CLIENT_ID'] = NexusOAuthService.CLIENT_ID
|
||||
self.logger.debug(f"Set NEXUS_OAUTH_INFO and NEXUS_OAUTH_CLIENT_ID={NexusOAuthService.CLIENT_ID} for engine (supports auto-refresh)")
|
||||
if api_key:
|
||||
os.environ['NEXUS_API_KEY'] = api_key
|
||||
elif api_key:
|
||||
os.environ['NEXUS_API_KEY'] = api_key
|
||||
self.logger.debug(f"Set NEXUS_API_KEY for engine (no auto-refresh)")
|
||||
else:
|
||||
if 'NEXUS_API_KEY' in os.environ:
|
||||
del os.environ['NEXUS_API_KEY']
|
||||
if 'NEXUS_OAUTH_INFO' in os.environ:
|
||||
del os.environ['NEXUS_OAUTH_INFO']
|
||||
if 'NEXUS_OAUTH_CLIENT_ID' in os.environ:
|
||||
del os.environ['NEXUS_OAUTH_CLIENT_ID']
|
||||
self.logger.debug(f"No Nexus auth available, cleared inherited env vars")
|
||||
|
||||
os.environ['DOTNET_SYSTEM_GLOBALIZATION_INVARIANT'] = "1"
|
||||
self.logger.debug(f"Temporarily set os.environ['DOTNET_SYSTEM_GLOBALIZATION_INVARIANT'] = '1' for engine call.")
|
||||
|
||||
self.logger.info("Environment prepared for jackify-engine install process by modifying os.environ.")
|
||||
self.logger.debug(f"NEXUS_API_KEY in os.environ (pre-call): {'[SET]' if os.environ.get('NEXUS_API_KEY') else '[NOT SET]'}")
|
||||
self.logger.debug(f"NEXUS_OAUTH_INFO in os.environ (pre-call): {'[SET]' if os.environ.get('NEXUS_OAUTH_INFO') else '[NOT SET]'}")
|
||||
|
||||
pretty_cmd = ' '.join([f'"{arg}"' if ' ' in arg else arg for arg in cmd])
|
||||
print(f"{COLOR_INFO}Launching Jackify Install Engine with command:{COLOR_RESET} {pretty_cmd}")
|
||||
|
||||
from jackify.backend.handlers.subprocess_utils import increase_file_descriptor_limit
|
||||
success, old_limit, new_limit, message = increase_file_descriptor_limit()
|
||||
if success:
|
||||
self.logger.debug(f"File descriptor limit: {message}")
|
||||
else:
|
||||
self.logger.warning(f"File descriptor limit: {message}")
|
||||
|
||||
from jackify.backend.handlers.subprocess_utils import get_clean_subprocess_env
|
||||
clean_env = get_clean_subprocess_env()
|
||||
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 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():
|
||||
line = parts[0].rstrip()
|
||||
else:
|
||||
buffer = b''
|
||||
continue
|
||||
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:
|
||||
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:
|
||||
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
|
||||
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}.")
|
||||
return
|
||||
self.logger.info(f"Engine completed with code {proc.returncode}.")
|
||||
except Exception as e:
|
||||
error_message = str(e)
|
||||
print(f"{COLOR_ERROR}Error running Jackify Install Engine: {error_message}{COLOR_RESET}\n")
|
||||
self.logger.error(f"Exception running engine: {error_message}", exc_info=True)
|
||||
|
||||
try:
|
||||
from jackify.backend.services.resource_manager import handle_file_descriptor_error
|
||||
if any(indicator in error_message.lower() for indicator in ['too many open files', 'emfile', 'resource temporarily unavailable']):
|
||||
result = handle_file_descriptor_error(error_message, "Jackify Install Engine execution")
|
||||
if result['auto_fix_success']:
|
||||
print(f"{COLOR_INFO}File descriptor limit increased automatically. {result['recommendation']}{COLOR_RESET}")
|
||||
self.logger.info(f"File descriptor limit increased automatically. {result['recommendation']}")
|
||||
elif result['error_detected']:
|
||||
print(f"{COLOR_WARNING}File descriptor limit issue detected. {result['recommendation']}{COLOR_RESET}")
|
||||
self.logger.warning(f"File descriptor limit issue detected but automatic fix failed. {result['recommendation']}")
|
||||
if result['manual_instructions']:
|
||||
distro = result['manual_instructions']['distribution']
|
||||
print(f"{COLOR_INFO}Manual ulimit increase instructions available for {distro} distribution{COLOR_RESET}")
|
||||
self.logger.info(f"Manual ulimit increase instructions available for {distro} distribution")
|
||||
except Exception as resource_error:
|
||||
self.logger.debug(f"Error checking for resource limit issues: {resource_error}")
|
||||
|
||||
return
|
||||
finally:
|
||||
for key, original_value in original_env_values.items():
|
||||
current_value_in_os_environ = os.environ.get(key)
|
||||
|
||||
display_original_value = f"'[REDACTED]'" if key == 'NEXUS_API_KEY' else f"'{original_value}'"
|
||||
|
||||
if original_value is not None:
|
||||
if current_value_in_os_environ != original_value:
|
||||
os.environ[key] = original_value
|
||||
self.logger.debug(f"Restored os.environ['{key}'] to its original value: {display_original_value}.")
|
||||
else:
|
||||
os.environ[key] = original_value
|
||||
self.logger.debug(f"os.environ['{key}'] ('{display_original_value}') matched original value. Ensured restoration.")
|
||||
else:
|
||||
if key in os.environ:
|
||||
self.logger.debug(f"Original os.environ['{key}'] was not set. Removing current value ('{'[REDACTED]' if os.environ.get(key) and key == 'NEXUS_API_KEY' else os.environ.get(key)}') that was set for the call.")
|
||||
del os.environ[key]
|
||||
|
||||
except Exception as e:
|
||||
error_message = str(e)
|
||||
print(f"{COLOR_ERROR}Error during installation workflow: {error_message}{COLOR_RESET}\n")
|
||||
self.logger.error(f"Exception in installation workflow: {error_message}", exc_info=True)
|
||||
|
||||
try:
|
||||
from jackify.backend.services.resource_manager import handle_file_descriptor_error
|
||||
if any(indicator in error_message.lower() for indicator in ['too many open files', 'emfile', 'resource temporarily unavailable']):
|
||||
result = handle_file_descriptor_error(error_message, "installation workflow")
|
||||
if result['auto_fix_success']:
|
||||
print(f"{COLOR_INFO}File descriptor limit increased automatically. {result['recommendation']}{COLOR_RESET}")
|
||||
self.logger.info(f"File descriptor limit increased automatically. {result['recommendation']}")
|
||||
elif result['error_detected']:
|
||||
print(f"{COLOR_WARNING}File descriptor limit issue detected. {result['recommendation']}{COLOR_RESET}")
|
||||
self.logger.warning(f"File descriptor limit issue detected but automatic fix failed. {result['recommendation']}")
|
||||
if result['manual_instructions']:
|
||||
distro = result['manual_instructions']['distribution']
|
||||
print(f"{COLOR_INFO}Manual ulimit increase instructions available for {distro} distribution{COLOR_RESET}")
|
||||
self.logger.info(f"Manual ulimit increase instructions available for {distro} distribution")
|
||||
except Exception as resource_error:
|
||||
self.logger.debug(f"Error checking for resource limit issues: {resource_error}")
|
||||
|
||||
return
|
||||
finally:
|
||||
sys.stdout = orig_stdout
|
||||
sys.stderr = orig_stderr
|
||||
workflow_log.close()
|
||||
|
||||
elapsed = int(time.time() - start_time)
|
||||
print(f"\nElapsed time: {elapsed//3600:02d}:{(elapsed%3600)//60:02d}:{elapsed%60:02d} (hh:mm:ss)\n")
|
||||
print(f"{COLOR_INFO}Your modlist has been installed to: {install_dir_str}{COLOR_RESET}\n")
|
||||
if self.context.get('machineid') != 'Tuxborn/Tuxborn':
|
||||
print(f"{COLOR_WARNING}Only Skyrim, Fallout 4, Fallout New Vegas, Oblivion, Starfield, and Oblivion Remastered modlists are compatible with Jackify's post-install configuration. Any modlist can be downloaded/installed, but only these games are supported for automated configuration.{COLOR_RESET}")
|
||||
|
||||
self.logger.debug("configuration_phase: Starting post-install game detection...")
|
||||
|
||||
modorganizer_ini = os.path.join(install_dir_str, "ModOrganizer.ini")
|
||||
detected_game = None
|
||||
self.logger.debug(f"configuration_phase: Looking for ModOrganizer.ini at: {modorganizer_ini}")
|
||||
if os.path.isfile(modorganizer_ini):
|
||||
self.logger.debug("configuration_phase: Found ModOrganizer.ini, detecting game...")
|
||||
from ..handlers.modlist_handler import ModlistHandler
|
||||
handler = ModlistHandler({}, steamdeck=self.steamdeck)
|
||||
handler.modlist_ini = modorganizer_ini
|
||||
handler.modlist_dir = install_dir_str
|
||||
if handler._detect_game_variables():
|
||||
detected_game = handler.game_var_full
|
||||
self.logger.debug(f"configuration_phase: Detected game: {detected_game}")
|
||||
else:
|
||||
self.logger.debug("configuration_phase: Failed to detect game variables")
|
||||
else:
|
||||
self.logger.debug("configuration_phase: ModOrganizer.ini not found")
|
||||
|
||||
supported_games = ["Skyrim Special Edition", "Fallout 4", "Fallout New Vegas", "Oblivion", "Starfield", "Oblivion Remastered", "Enderal"]
|
||||
is_tuxborn = self.context.get('machineid') == 'Tuxborn/Tuxborn'
|
||||
self.logger.debug(f"configuration_phase: detected_game='{detected_game}', is_tuxborn={is_tuxborn}")
|
||||
self.logger.debug(f"configuration_phase: Checking condition: (detected_game in supported_games) or is_tuxborn")
|
||||
self.logger.debug(f"configuration_phase: Result: {(detected_game in supported_games) or is_tuxborn}")
|
||||
|
||||
if (detected_game in supported_games) or is_tuxborn:
|
||||
self.logger.debug("configuration_phase: Entering Steam configuration workflow...")
|
||||
shortcut_name = self.context.get('modlist_name')
|
||||
self.logger.debug(f"configuration_phase: shortcut_name from context: '{shortcut_name}'")
|
||||
|
||||
if is_tuxborn and not shortcut_name:
|
||||
self.logger.warning("Tuxborn is true, but shortcut_name (modlist_name in context) is missing. Defaulting to 'Tuxborn Automatic Installer'")
|
||||
shortcut_name = "Tuxborn Automatic Installer"
|
||||
elif not shortcut_name:
|
||||
print("\n" + "-" * 28)
|
||||
print(f"{COLOR_PROMPT}Please provide a name for the Steam shortcut for '{self.context.get('modlist_name', 'this modlist')}'.{COLOR_RESET}")
|
||||
raw_shortcut_name = input(f"{COLOR_PROMPT}Steam Shortcut Name (or 'q' to cancel): {COLOR_RESET} ").strip()
|
||||
if raw_shortcut_name.lower() == 'q' or not raw_shortcut_name:
|
||||
self.logger.debug("configuration_phase: User cancelled shortcut name input")
|
||||
return
|
||||
shortcut_name = raw_shortcut_name
|
||||
|
||||
self.logger.debug(f"configuration_phase: Final shortcut_name: '{shortcut_name}'")
|
||||
|
||||
is_gui_mode = os.environ.get('JACKIFY_GUI_MODE') == '1'
|
||||
self.logger.debug(f"configuration_phase: is_gui_mode={is_gui_mode}")
|
||||
|
||||
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? "
|
||||
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}'")
|
||||
|
||||
if configure_choice == 'n':
|
||||
print(f"{COLOR_INFO}Skipping Steam configuration. You can configure it later using 'Configure New Modlist'.{COLOR_RESET}")
|
||||
self.logger.debug("configuration_phase: User chose to skip Steam configuration")
|
||||
return
|
||||
else:
|
||||
self.logger.debug("configuration_phase: In GUI mode, proceeding automatically...")
|
||||
|
||||
self.logger.debug("configuration_phase: Proceeding with Steam configuration...")
|
||||
|
||||
if not is_gui_mode:
|
||||
from jackify.backend.handlers.resolution_handler import ResolutionHandler
|
||||
resolution_handler = ResolutionHandler()
|
||||
|
||||
is_steamdeck = self.steamdeck if hasattr(self, 'steamdeck') else False
|
||||
|
||||
selected_resolution = resolution_handler.select_resolution(steamdeck=is_steamdeck)
|
||||
if selected_resolution:
|
||||
self.context['resolution'] = selected_resolution
|
||||
self.logger.info(f"Resolution set to: {selected_resolution}")
|
||||
|
||||
self.logger.info(f"Starting Steam configuration for '{shortcut_name}'")
|
||||
|
||||
mo2_exe_path = os.path.join(install_dir_str, 'ModOrganizer.exe')
|
||||
|
||||
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}")
|
||||
|
||||
from ..services.automated_prefix_service import AutomatedPrefixService
|
||||
prefix_service = AutomatedPrefixService()
|
||||
|
||||
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:
|
||||
_is_steamdeck = False
|
||||
if os.path.exists('/etc/os-release'):
|
||||
with open('/etc/os-release') as f:
|
||||
if 'steamdeck' in f.read().lower():
|
||||
_is_steamdeck = True
|
||||
except Exception:
|
||||
_is_steamdeck = False
|
||||
result = prefix_service.run_working_workflow(
|
||||
shortcut_name, install_dir_str, mo2_exe_path, progress_callback, steamdeck=_is_steamdeck
|
||||
)
|
||||
|
||||
if isinstance(result, tuple) and len(result) == 4:
|
||||
if result[0] == "CONFLICT":
|
||||
conflicts = result[1]
|
||||
print(f"\n{COLOR_WARNING}Found existing Steam shortcut(s) with the same name and path:{COLOR_RESET}")
|
||||
|
||||
for i, conflict in enumerate(conflicts, 1):
|
||||
print(f" {i}. Name: {conflict['name']}")
|
||||
print(f" Executable: {conflict['exe']}")
|
||||
print(f" Start Directory: {conflict['startdir']}")
|
||||
|
||||
print(f"\n{COLOR_PROMPT}Options:{COLOR_RESET}")
|
||||
print(" * Replace - Remove the existing shortcut and create a new one")
|
||||
print(" * Cancel - Keep the existing shortcut and stop the installation")
|
||||
print(" * Skip - Continue without creating a Steam shortcut")
|
||||
|
||||
choice = input(f"\n{COLOR_PROMPT}Choose an option (replace/cancel/skip): {COLOR_RESET}").strip().lower()
|
||||
|
||||
if choice == 'replace':
|
||||
print(f"{COLOR_INFO}Replacing existing shortcut...{COLOR_RESET}")
|
||||
success, app_id = prefix_service.replace_existing_shortcut(shortcut_name, mo2_exe_path, install_dir_str)
|
||||
if success and app_id:
|
||||
result = prefix_service.continue_workflow_after_conflict_resolution(
|
||||
shortcut_name, install_dir_str, mo2_exe_path, app_id, progress_callback
|
||||
)
|
||||
if isinstance(result, tuple) and len(result) >= 3:
|
||||
success, prefix_path, app_id = result[0], result[1], result[2]
|
||||
else:
|
||||
success, prefix_path, app_id = False, None, None
|
||||
else:
|
||||
success, prefix_path, app_id = False, None, None
|
||||
elif choice == 'cancel':
|
||||
print(f"{COLOR_INFO}Cancelling installation.{COLOR_RESET}")
|
||||
return
|
||||
elif choice == 'skip':
|
||||
print(f"{COLOR_INFO}Skipping Steam shortcut creation.{COLOR_RESET}")
|
||||
success, prefix_path, app_id = True, None, None
|
||||
else:
|
||||
print(f"{COLOR_ERROR}Invalid choice. Cancelling.{COLOR_RESET}")
|
||||
return
|
||||
else:
|
||||
success, prefix_path, app_id, last_timestamp = result
|
||||
elif isinstance(result, tuple) and len(result) == 3:
|
||||
if result[0] == "CONFLICT":
|
||||
conflicts = result[1]
|
||||
print(f"\n{COLOR_WARNING}Found existing Steam shortcut(s) with the same name and path:{COLOR_RESET}")
|
||||
|
||||
for i, conflict in enumerate(conflicts, 1):
|
||||
print(f" {i}. Name: {conflict['name']}")
|
||||
print(f" Executable: {conflict['exe']}")
|
||||
print(f" Start Directory: {conflict['startdir']}")
|
||||
|
||||
print(f"\n{COLOR_PROMPT}Options:{COLOR_RESET}")
|
||||
print(" * Replace - Remove the existing shortcut and create a new one")
|
||||
print(" * Cancel - Keep the existing shortcut and stop the installation")
|
||||
print(" * Skip - Continue without creating a Steam shortcut")
|
||||
|
||||
choice = input(f"\n{COLOR_PROMPT}Choose an option (replace/cancel/skip): {COLOR_RESET}").strip().lower()
|
||||
|
||||
if choice == 'replace':
|
||||
print(f"{COLOR_INFO}Replacing existing shortcut...{COLOR_RESET}")
|
||||
success, app_id = prefix_service.replace_existing_shortcut(shortcut_name, mo2_exe_path, install_dir_str)
|
||||
if success and app_id:
|
||||
result = prefix_service.continue_workflow_after_conflict_resolution(
|
||||
shortcut_name, install_dir_str, mo2_exe_path, app_id, progress_callback
|
||||
)
|
||||
if isinstance(result, tuple) and len(result) >= 3:
|
||||
success, prefix_path, app_id = result[0], result[1], result[2]
|
||||
else:
|
||||
success, prefix_path, app_id = False, None, None
|
||||
else:
|
||||
success, prefix_path, app_id = False, None, None
|
||||
elif choice == 'cancel':
|
||||
print(f"{COLOR_INFO}Cancelling installation.{COLOR_RESET}")
|
||||
return
|
||||
elif choice == 'skip':
|
||||
print(f"{COLOR_INFO}Skipping Steam shortcut creation.{COLOR_RESET}")
|
||||
success, prefix_path, app_id = True, None, None
|
||||
else:
|
||||
print(f"{COLOR_ERROR}Invalid choice. Cancelling.{COLOR_RESET}")
|
||||
return
|
||||
else:
|
||||
success, prefix_path, app_id = result
|
||||
else:
|
||||
if result is True:
|
||||
success, prefix_path, app_id = True, None, None
|
||||
else:
|
||||
success, prefix_path, app_id = False, None, None
|
||||
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}")
|
||||
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
|
||||
|
||||
from jackify.backend.services.modlist_service import ModlistService
|
||||
from jackify.backend.models.modlist import ModlistContext
|
||||
|
||||
modlist_context = ModlistContext(
|
||||
name=shortcut_name,
|
||||
install_dir=Path(install_dir_str),
|
||||
download_dir=Path(install_dir_str) / "downloads",
|
||||
game_type=self.context.get('detected_game', 'Unknown'),
|
||||
nexus_api_key='',
|
||||
modlist_value=self.context.get('modlist_value', ''),
|
||||
modlist_source=self.context.get('modlist_source', 'identifier'),
|
||||
resolution=self.context.get('resolution'),
|
||||
mo2_exe_path=Path(mo2_exe_path),
|
||||
skip_confirmation=True,
|
||||
engine_installed=True
|
||||
)
|
||||
|
||||
modlist_context.app_id = app_id
|
||||
|
||||
modlist_service = ModlistService(self.system_info)
|
||||
|
||||
if 'progress_callback' in locals() and progress_callback:
|
||||
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")
|
||||
|
||||
configuration_success = modlist_service.configure_modlist_post_steam(modlist_context)
|
||||
|
||||
if configuration_success:
|
||||
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")
|
||||
else:
|
||||
print(f"{COLOR_INFO}Modlist installation complete.{COLOR_RESET}")
|
||||
if detected_game:
|
||||
print(f"{COLOR_WARNING}Detected game '{detected_game}' is not supported for automated Steam configuration.{COLOR_RESET}")
|
||||
else:
|
||||
print(f"{COLOR_WARNING}Could not detect game type from ModOrganizer.ini for automated configuration.{COLOR_RESET}")
|
||||
print(f"{COLOR_INFO}You may need to manually configure the modlist for Steam/Proton.{COLOR_RESET}")
|
||||
170
jackify/backend/core/modlist_operations_configuration_gui.py
Normal file
@@ -0,0 +1,170 @@
|
||||
"""GUI configuration phase methods for ModlistInstallCLI (Mixin)."""
|
||||
import logging
|
||||
import os
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ModlistOperationsConfigurationGUIMixin:
|
||||
"""Mixin providing GUI configuration phase methods."""
|
||||
|
||||
def configuration_phase_gui_mode(self, context,
|
||||
progress_callback=None,
|
||||
manual_steps_callback=None,
|
||||
completion_callback=None):
|
||||
"""
|
||||
GUI-friendly configuration phase that uses callbacks instead of prompts.
|
||||
|
||||
This method provides the same functionality as configuration_phase() but
|
||||
integrates with GUI frontends using Qt callbacks instead of CLI prompts.
|
||||
|
||||
Args:
|
||||
context: Configuration context dict with modlist details
|
||||
progress_callback: Called with progress messages (str)
|
||||
manual_steps_callback: Called when manual steps needed (modlist_name, retry_count)
|
||||
completion_callback: Called when configuration completes (success, message, modlist_name)
|
||||
"""
|
||||
try:
|
||||
from .modlist_operations import _get_user_proton_version
|
||||
|
||||
original_gui_mode = os.environ.get('JACKIFY_GUI_MODE')
|
||||
|
||||
try:
|
||||
config_context = {
|
||||
'name': context.get('modlist_name', ''),
|
||||
'path': context.get('install_dir', ''),
|
||||
'mo2_exe_path': context.get('mo2_exe_path', ''),
|
||||
'modlist_value': context.get('modlist_value'),
|
||||
'modlist_source': context.get('modlist_source'),
|
||||
'resolution': context.get('resolution'),
|
||||
'skip_confirmation': True,
|
||||
'manual_steps_completed': False
|
||||
}
|
||||
|
||||
existing_app_id = context.get('app_id')
|
||||
if existing_app_id:
|
||||
config_context['appid'] = existing_app_id
|
||||
|
||||
if progress_callback:
|
||||
progress_callback(f"Configuring existing modlist with AppID {existing_app_id}...")
|
||||
|
||||
from jackify.backend.handlers.menu_handler import ModlistMenuHandler
|
||||
from jackify.backend.handlers.config_handler import ConfigHandler
|
||||
|
||||
config_handler = ConfigHandler()
|
||||
modlist_menu = ModlistMenuHandler(config_handler)
|
||||
|
||||
retry_count = 0
|
||||
max_retries = 3
|
||||
|
||||
while retry_count < max_retries:
|
||||
if progress_callback:
|
||||
progress_callback("Running modlist configuration...")
|
||||
|
||||
result = modlist_menu.run_modlist_configuration_phase(config_context)
|
||||
|
||||
if progress_callback:
|
||||
progress_callback(f"Configuration attempt {retry_count}: {'Success' if result else 'Failed'}")
|
||||
|
||||
if result:
|
||||
if completion_callback:
|
||||
completion_callback(True, "Core configuration complete", config_context['name'])
|
||||
return True
|
||||
else:
|
||||
retry_count += 1
|
||||
|
||||
if retry_count < max_retries:
|
||||
if progress_callback:
|
||||
progress_callback(f"Configuration failed on attempt {retry_count}, showing manual steps dialog...")
|
||||
if manual_steps_callback:
|
||||
if progress_callback:
|
||||
progress_callback(f"Calling manual_steps_callback for {config_context['name']}, retry {retry_count}")
|
||||
manual_steps_callback(config_context['name'], retry_count)
|
||||
|
||||
config_context['manual_steps_completed'] = True
|
||||
else:
|
||||
if completion_callback:
|
||||
completion_callback(False, "Manual steps failed after multiple attempts", config_context['name'])
|
||||
return False
|
||||
|
||||
if completion_callback:
|
||||
completion_callback(False, "Configuration failed", config_context['name'])
|
||||
return False
|
||||
|
||||
else:
|
||||
from jackify.backend.handlers.menu_handler import ModlistMenuHandler
|
||||
from jackify.backend.handlers.config_handler import ConfigHandler
|
||||
|
||||
config_handler = ConfigHandler()
|
||||
modlist_menu = ModlistMenuHandler(config_handler)
|
||||
|
||||
if progress_callback:
|
||||
progress_callback("Creating Steam shortcut...")
|
||||
|
||||
from jackify.backend.services.native_steam_service import NativeSteamService
|
||||
steam_service = NativeSteamService()
|
||||
|
||||
proton_version = _get_user_proton_version()
|
||||
|
||||
success, app_id = steam_service.create_shortcut_with_proton(
|
||||
app_name=config_context['name'],
|
||||
exe_path=config_context['mo2_exe_path'],
|
||||
start_dir=os.path.dirname(config_context['mo2_exe_path']),
|
||||
launch_options="%command%",
|
||||
tags=["Jackify"],
|
||||
proton_version=proton_version
|
||||
)
|
||||
|
||||
if not success or not app_id:
|
||||
if completion_callback:
|
||||
completion_callback(False, "Failed to create Steam shortcut", config_context['name'])
|
||||
return False
|
||||
|
||||
config_context['appid'] = app_id
|
||||
|
||||
if progress_callback:
|
||||
from jackify.shared.timing import get_timestamp
|
||||
progress_callback(f"{get_timestamp()} Steam shortcut created successfully")
|
||||
|
||||
if progress_callback:
|
||||
progress_callback("Running modlist configuration...")
|
||||
|
||||
if progress_callback:
|
||||
progress_callback(f"About to call run_modlist_configuration_phase with context: {config_context}")
|
||||
|
||||
result = modlist_menu.run_modlist_configuration_phase(config_context)
|
||||
|
||||
if progress_callback:
|
||||
progress_callback(f"run_modlist_configuration_phase returned: {result}")
|
||||
|
||||
if result:
|
||||
if completion_callback:
|
||||
completion_callback(True, "Core configuration complete", config_context['name'])
|
||||
return True
|
||||
else:
|
||||
if progress_callback:
|
||||
progress_callback("Configuration failed, manual Steam/Proton setup required")
|
||||
if manual_steps_callback:
|
||||
if progress_callback:
|
||||
progress_callback(f"About to call manual_steps_callback for {config_context['name']}, retry 1")
|
||||
manual_steps_callback(config_context['name'], 1)
|
||||
if progress_callback:
|
||||
progress_callback("manual_steps_callback completed")
|
||||
|
||||
return True
|
||||
|
||||
if completion_callback:
|
||||
completion_callback(False, "Configuration failed", config_context['name'])
|
||||
return False
|
||||
|
||||
finally:
|
||||
if original_gui_mode is not None:
|
||||
os.environ['JACKIFY_GUI_MODE'] = original_gui_mode
|
||||
else:
|
||||
os.environ.pop('JACKIFY_GUI_MODE', None)
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Configuration failed: {str(e)}"
|
||||
if completion_callback:
|
||||
completion_callback(False, error_msg, context.get('modlist_name', 'Unknown'))
|
||||
return False
|
||||
408
jackify/backend/core/modlist_operations_discovery.py
Normal file
@@ -0,0 +1,408 @@
|
||||
"""Discovery phase methods for ModlistInstallCLI (Mixin)."""
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict
|
||||
|
||||
from ..handlers.ui_colors import (
|
||||
COLOR_PROMPT,
|
||||
COLOR_RESET,
|
||||
COLOR_INFO,
|
||||
COLOR_ERROR,
|
||||
COLOR_SUCCESS,
|
||||
COLOR_WARNING,
|
||||
COLOR_SELECTION,
|
||||
)
|
||||
from ..handlers.config_handler import ConfigHandler
|
||||
from jackify.backend.models.configuration import SystemInfo
|
||||
from jackify.backend.services.modlist_service import ModlistService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ModlistOperationsDiscoveryMixin:
|
||||
"""Mixin providing modlist discovery phase methods."""
|
||||
|
||||
def run_discovery_phase(self, context_override=None) -> Optional[Dict]:
|
||||
"""
|
||||
Run the discovery phase: prompt for all required info, and validate inputs.
|
||||
Returns a context dict with all collected info, or None if cancelled.
|
||||
Accepts context_override for pre-filled values (e.g., for Tuxborn/machineid flow).
|
||||
"""
|
||||
from .modlist_operations import get_jackify_engine_path
|
||||
|
||||
self.logger.info("Starting modlist discovery phase (restored logic).")
|
||||
print(f"\n{COLOR_PROMPT}--- Wabbajack Modlist Install: Discovery Phase ---{COLOR_RESET}")
|
||||
|
||||
if context_override:
|
||||
self.context.update(context_override)
|
||||
if 'resolution' in context_override:
|
||||
self.context['resolution'] = context_override['resolution']
|
||||
else:
|
||||
self.context = {}
|
||||
|
||||
is_gui_mode = os.environ.get('JACKIFY_GUI_MODE') == '1'
|
||||
if self.context.get('machineid'):
|
||||
required_keys = ['modlist_name', 'install_dir', 'download_dir', 'nexus_api_key']
|
||||
else:
|
||||
required_keys = ['modlist_name', 'install_dir', 'download_dir', 'nexus_api_key', 'game_type']
|
||||
has_modlist = self.context.get('modlist_value') or self.context.get('machineid')
|
||||
missing = [k for k in required_keys if not self.context.get(k)]
|
||||
if is_gui_mode:
|
||||
if missing or not has_modlist:
|
||||
self.logger.error(f"Missing required arguments for GUI workflow: {', '.join(missing)}")
|
||||
if not has_modlist:
|
||||
self.logger.error("Missing modlist_value or machineid for GUI workflow.")
|
||||
self.logger.error("This workflow must be fully non-interactive. Please report this as a bug if you see this message.")
|
||||
return None
|
||||
self.logger.info("All required context present in GUI mode, skipping prompts.")
|
||||
return self.context
|
||||
|
||||
engine_executable = get_jackify_engine_path()
|
||||
self.logger.debug(f"Engine executable path: {engine_executable}")
|
||||
|
||||
if not os.path.exists(engine_executable):
|
||||
print(f"{COLOR_ERROR}Error: jackify-install-engine not found at expected location.{COLOR_RESET}")
|
||||
print(f"{COLOR_INFO}Expected: {engine_executable}{COLOR_RESET}")
|
||||
return None
|
||||
|
||||
engine_dir = os.path.dirname(engine_executable)
|
||||
|
||||
if 'machineid' not in self.context:
|
||||
print("\n" + "-" * 28)
|
||||
print(f"{COLOR_PROMPT}How would you like to select your modlist?{COLOR_RESET}")
|
||||
print(f"{COLOR_SELECTION}1.{COLOR_RESET} Select from a list of available modlists")
|
||||
print(f"{COLOR_SELECTION}2.{COLOR_RESET} Provide the path to a .wabbajack file on disk")
|
||||
print(f"{COLOR_SELECTION}0.{COLOR_RESET} Cancel and return to previous menu")
|
||||
source_choice = input(f"{COLOR_PROMPT}Enter your selection (0-2): {COLOR_RESET}").strip()
|
||||
self.logger.debug(f"User selected modlist source option: {source_choice}")
|
||||
|
||||
if source_choice == '1':
|
||||
self.context['modlist_source_type'] = 'online_list'
|
||||
print(f"\n{COLOR_INFO}Fetching available modlists... This may take a moment.{COLOR_RESET}")
|
||||
try:
|
||||
is_steamdeck = False
|
||||
if os.path.exists('/etc/os-release'):
|
||||
with open('/etc/os-release') as f:
|
||||
if 'steamdeck' in f.read().lower():
|
||||
is_steamdeck = True
|
||||
system_info = SystemInfo(is_steamdeck=is_steamdeck)
|
||||
modlist_service = ModlistService(system_info)
|
||||
|
||||
categories = [
|
||||
("Skyrim", "skyrim"),
|
||||
("Fallout 4", "fallout4"),
|
||||
("Fallout New Vegas", "falloutnv"),
|
||||
("Oblivion", "oblivion"),
|
||||
("Starfield", "starfield"),
|
||||
("Oblivion Remastered", "oblivion_remastered"),
|
||||
("Other Games", "other")
|
||||
]
|
||||
grouped_modlists = {}
|
||||
for label, key in categories:
|
||||
grouped_modlists[label] = modlist_service.list_modlists(game_type=key)
|
||||
|
||||
selected_modlist_info = None
|
||||
while not selected_modlist_info:
|
||||
print(f"\n{COLOR_PROMPT}Select a game category:{COLOR_RESET}")
|
||||
category_display_map = {}
|
||||
display_idx = 1
|
||||
for label, _ in categories:
|
||||
modlists = grouped_modlists[label]
|
||||
if label == "Oblivion Remastered" or modlists:
|
||||
print(f" {COLOR_SELECTION}{display_idx}.{COLOR_RESET} {label} ({len(modlists)} modlists)")
|
||||
category_display_map[str(display_idx)] = label
|
||||
display_idx += 1
|
||||
if display_idx == 1:
|
||||
print(f"{COLOR_WARNING}No modlists found to display after grouping. Engine output might be empty or filtered entirely.{COLOR_RESET}")
|
||||
return None
|
||||
print(f" {COLOR_SELECTION}0.{COLOR_RESET} Cancel")
|
||||
game_cat_choice = input(f"{COLOR_PROMPT}Enter selection: {COLOR_RESET}").strip()
|
||||
if game_cat_choice == '0':
|
||||
self.logger.info("User cancelled game category selection.")
|
||||
return None
|
||||
actual_label = category_display_map.get(game_cat_choice)
|
||||
if not actual_label:
|
||||
print(f"{COLOR_ERROR}Invalid selection. Please try again.{COLOR_RESET}")
|
||||
continue
|
||||
modlist_group_for_game = sorted(grouped_modlists[actual_label], key=lambda x: x.id.lower())
|
||||
print(f"\n{COLOR_SUCCESS}Available Modlists for {actual_label}:{COLOR_RESET}")
|
||||
for idx, m_detail in enumerate(modlist_group_for_game, 1):
|
||||
if actual_label == "Other Games":
|
||||
print(f" {COLOR_SELECTION}{idx}.{COLOR_RESET} {m_detail.id} ({m_detail.game})")
|
||||
else:
|
||||
print(f" {COLOR_SELECTION}{idx}.{COLOR_RESET} {m_detail.id}")
|
||||
print(f" {COLOR_SELECTION}0.{COLOR_RESET} Back to game categories")
|
||||
while True:
|
||||
mod_choice_idx_str = input(f"{COLOR_PROMPT}Select modlist (or 0): {COLOR_RESET}").strip()
|
||||
if mod_choice_idx_str == '0':
|
||||
break
|
||||
if mod_choice_idx_str.isdigit():
|
||||
mod_idx = int(mod_choice_idx_str) - 1
|
||||
if 0 <= mod_idx < len(modlist_group_for_game):
|
||||
selected_modlist_info = {
|
||||
'id': modlist_group_for_game[mod_idx].id,
|
||||
'game': modlist_group_for_game[mod_idx].game,
|
||||
'machine_url': getattr(modlist_group_for_game[mod_idx], 'machine_url', modlist_group_for_game[mod_idx].id)
|
||||
}
|
||||
self.context['modlist_source'] = 'identifier'
|
||||
self.context['modlist_value'] = selected_modlist_info.get('machine_url', selected_modlist_info['id'])
|
||||
self.context['modlist_game'] = selected_modlist_info['game']
|
||||
self.context['modlist_name_suggestion'] = selected_modlist_info['id'].split('/')[-1]
|
||||
self.logger.info(f"User selected online modlist: {selected_modlist_info}")
|
||||
break
|
||||
else:
|
||||
print(f"{COLOR_ERROR}Invalid modlist number.{COLOR_RESET}")
|
||||
else:
|
||||
print(f"{COLOR_ERROR}Invalid input. Please enter a number.{COLOR_RESET}")
|
||||
if selected_modlist_info:
|
||||
break
|
||||
except Exception as e:
|
||||
self.logger.error(f"Unexpected error fetching modlists: {e}", exc_info=True)
|
||||
print(f"{COLOR_ERROR}Unexpected error fetching modlists: {e}{COLOR_RESET}")
|
||||
return None
|
||||
|
||||
elif source_choice == '2':
|
||||
self.context['modlist_source_type'] = 'local_file'
|
||||
print(f"\n{COLOR_PROMPT}Please provide the path to your .wabbajack file (tab-completion supported).{COLOR_RESET}")
|
||||
modlist_path = self.menu_handler.get_existing_file_path(
|
||||
prompt_message="Enter the path to your .wabbajack file (or 'q' to cancel):",
|
||||
extension_filter=".wabbajack",
|
||||
no_header=True
|
||||
)
|
||||
if modlist_path is None:
|
||||
self.logger.info("User cancelled .wabbajack file selection.")
|
||||
print(f"{COLOR_INFO}Cancelled by user.{COLOR_RESET}")
|
||||
return None
|
||||
|
||||
self.context['modlist_source'] = 'path'
|
||||
self.context['modlist_value'] = str(modlist_path)
|
||||
self.context['modlist_name_suggestion'] = Path(modlist_path).stem
|
||||
self.logger.info(f"User selected local .wabbajack file: {modlist_path}")
|
||||
|
||||
elif source_choice == '0':
|
||||
self.logger.info("User cancelled modlist source selection.")
|
||||
print(f"{COLOR_INFO}Returning to previous menu.{COLOR_RESET}")
|
||||
return None
|
||||
else:
|
||||
self.logger.warning(f"Invalid modlist source choice: {source_choice}")
|
||||
print(f"{COLOR_ERROR}Invalid selection. Please try again.{COLOR_RESET}")
|
||||
return self.run_discovery_phase()
|
||||
|
||||
if 'modlist_name' not in self.context or not self.context['modlist_name']:
|
||||
default_name = self.context.get('modlist_name_suggestion', 'MyModlist')
|
||||
print("\n" + "-" * 28)
|
||||
print(f"{COLOR_PROMPT}Enter a name for this modlist installation in Steam.{COLOR_RESET}")
|
||||
print(f"{COLOR_INFO}(This will be the shortcut name. Default: {default_name}){COLOR_RESET}")
|
||||
modlist_name_input = input(f"{COLOR_PROMPT}Modlist Name (or 'q' to cancel): {COLOR_RESET}").strip()
|
||||
if not modlist_name_input:
|
||||
modlist_name = default_name
|
||||
elif modlist_name_input.lower() == 'q':
|
||||
self.logger.info("User cancelled at modlist name prompt.")
|
||||
return None
|
||||
else:
|
||||
modlist_name = modlist_name_input
|
||||
self.context['modlist_name'] = modlist_name
|
||||
self.logger.debug(f"Modlist name set to: {self.context['modlist_name']}")
|
||||
|
||||
if 'install_dir' not in self.context:
|
||||
config_handler = ConfigHandler()
|
||||
base_install_dir = Path(config_handler.get_modlist_install_base_dir())
|
||||
default_install_dir = base_install_dir / self.context['modlist_name']
|
||||
print("\n" + "-" * 28)
|
||||
print(f"{COLOR_PROMPT}Enter the main installation directory for '{self.context['modlist_name']}'.{COLOR_RESET}")
|
||||
print(f"{COLOR_INFO}(Default: {default_install_dir}){COLOR_RESET}")
|
||||
install_dir_path = self.menu_handler.get_directory_path(
|
||||
prompt_message=f"{COLOR_PROMPT}Install directory (or 'q' to cancel, Enter for default): {COLOR_RESET}",
|
||||
default_path=default_install_dir,
|
||||
create_if_missing=True,
|
||||
no_header=True
|
||||
)
|
||||
if install_dir_path is None:
|
||||
self.logger.info("User cancelled at install directory prompt.")
|
||||
return None
|
||||
self.context['install_dir'] = install_dir_path
|
||||
self.logger.debug(f"Install directory context set to: {self.context['install_dir']}")
|
||||
|
||||
if 'download_dir' not in self.context:
|
||||
config_handler = ConfigHandler()
|
||||
base_download_dir = Path(config_handler.get_modlist_downloads_base_dir())
|
||||
default_download_dir = base_download_dir / self.context['modlist_name']
|
||||
print("\n" + "-" * 28)
|
||||
print(f"{COLOR_PROMPT}Enter the downloads directory for modlist archives.{COLOR_RESET}")
|
||||
print(f"{COLOR_INFO}(Default: {default_download_dir}){COLOR_RESET}")
|
||||
download_dir_path = self.menu_handler.get_directory_path(
|
||||
prompt_message=f"{COLOR_PROMPT}Download directory (or 'q' to cancel, Enter for default): {COLOR_RESET}",
|
||||
default_path=default_download_dir,
|
||||
create_if_missing=True,
|
||||
no_header=True
|
||||
)
|
||||
if download_dir_path is None:
|
||||
self.logger.info("User cancelled at download directory prompt.")
|
||||
return None
|
||||
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()
|
||||
authenticated, method, username = auth_service.get_auth_status()
|
||||
|
||||
if authenticated:
|
||||
if method == 'oauth':
|
||||
print("\n" + "-" * 28)
|
||||
print(f"{COLOR_SUCCESS}Nexus Authentication: Authorized via OAuth{COLOR_RESET}")
|
||||
if username:
|
||||
print(f"{COLOR_INFO}Logged in as: {username}{COLOR_RESET}")
|
||||
elif method == 'api_key':
|
||||
print("\n" + "-" * 28)
|
||||
print(f"{COLOR_INFO}Nexus Authentication: Using API Key (Legacy){COLOR_RESET}")
|
||||
|
||||
api_key, oauth_info = auth_service.get_auth_for_engine()
|
||||
if api_key:
|
||||
self.context['nexus_api_key'] = api_key
|
||||
self.context['nexus_oauth_info'] = oauth_info
|
||||
else:
|
||||
print(f"\n{COLOR_WARNING}Your authentication has expired or is invalid.{COLOR_RESET}")
|
||||
authenticated = False
|
||||
|
||||
if not authenticated:
|
||||
print("\n" + "-" * 28)
|
||||
print(f"{COLOR_WARNING}Nexus Mods authentication is required for downloading mods.{COLOR_RESET}")
|
||||
print(f"\n{COLOR_PROMPT}Would you like to authorize with Nexus now?{COLOR_RESET}")
|
||||
print(f"{COLOR_INFO}This will open your browser for secure OAuth authorization.{COLOR_RESET}")
|
||||
|
||||
authorize = input(f"{COLOR_PROMPT}Authorize now? [Y/n]: {COLOR_RESET}").strip().lower()
|
||||
|
||||
if authorize in ('', 'y', 'yes'):
|
||||
print(f"\n{COLOR_INFO}Starting OAuth authorization...{COLOR_RESET}")
|
||||
print(f"{COLOR_WARNING}Your browser will open shortly.{COLOR_RESET}")
|
||||
print(f"{COLOR_INFO}Note: You may see a security warning about a self-signed certificate.{COLOR_RESET}")
|
||||
print(f"{COLOR_INFO}This is normal - click 'Advanced' and 'Proceed' to continue.{COLOR_RESET}")
|
||||
|
||||
def show_message(msg):
|
||||
print(f"\n{COLOR_INFO}{msg}{COLOR_RESET}")
|
||||
|
||||
success = auth_service.authorize_oauth(show_browser_message_callback=show_message)
|
||||
|
||||
if success:
|
||||
print(f"\n{COLOR_SUCCESS}OAuth authorization successful!{COLOR_RESET}")
|
||||
_, _, username = auth_service.get_auth_status()
|
||||
if username:
|
||||
print(f"{COLOR_INFO}Authorized as: {username}{COLOR_RESET}")
|
||||
|
||||
api_key, oauth_info = auth_service.get_auth_for_engine()
|
||||
if api_key:
|
||||
self.context['nexus_api_key'] = api_key
|
||||
self.context['nexus_oauth_info'] = oauth_info
|
||||
else:
|
||||
print(f"{COLOR_ERROR}Failed to retrieve auth token after authorization.{COLOR_RESET}")
|
||||
return None
|
||||
else:
|
||||
print(f"\n{COLOR_ERROR}OAuth authorization failed.{COLOR_RESET}")
|
||||
return None
|
||||
else:
|
||||
print(f"\n{COLOR_INFO}Authorization required to proceed. Installation cancelled.{COLOR_RESET}")
|
||||
self.logger.info("User declined Nexus authorization.")
|
||||
return None
|
||||
self.logger.debug("Nexus authentication configured for engine.")
|
||||
|
||||
self._display_summary()
|
||||
|
||||
game_type = None
|
||||
game_name = None
|
||||
if self.context.get('modlist_source_type') == 'online_list':
|
||||
game_name = self.context.get('modlist_game', '')
|
||||
game_mapping = {
|
||||
'skyrim special edition': 'skyrim',
|
||||
'skyrim': 'skyrim',
|
||||
'fallout 4': 'fallout4',
|
||||
'fallout new vegas': 'falloutnv',
|
||||
'oblivion': 'oblivion',
|
||||
'starfield': 'starfield',
|
||||
'oblivion remastered': 'oblivion_remastered'
|
||||
}
|
||||
game_type = game_mapping.get(game_name.lower())
|
||||
if not game_type:
|
||||
game_type = 'unknown'
|
||||
elif self.context.get('modlist_source_type') == 'local_file':
|
||||
wabbajack_path = self.context.get('modlist_value')
|
||||
if wabbajack_path:
|
||||
result = self.wabbajack_parser.parse_wabbajack_game_type(Path(wabbajack_path))
|
||||
if result:
|
||||
if isinstance(result, tuple):
|
||||
game_type, raw_game_type = result
|
||||
game_name = raw_game_type if game_type == 'unknown' else game_type
|
||||
else:
|
||||
game_type = result
|
||||
game_name = game_type
|
||||
|
||||
if game_type and not self.wabbajack_parser.is_supported_game(game_type):
|
||||
print("\n" + "─" * 46)
|
||||
print(" Game Support Notice\n")
|
||||
print(f"You are about to install a modlist for: {game_name or 'Unknown'}\n")
|
||||
print("Jackify does not provide post-install configuration for this game.")
|
||||
print("You can still install and use the modlist, but you will need to manually set up Steam shortcuts and other steps after installation.\n")
|
||||
print("Press [Enter] to continue, or [Ctrl+C] to cancel.")
|
||||
print("─" * 46 + "\n")
|
||||
try:
|
||||
input()
|
||||
except KeyboardInterrupt:
|
||||
print(f"{COLOR_INFO}Installation cancelled by user.{COLOR_RESET}")
|
||||
return None
|
||||
|
||||
if self.context.get('skip_confirmation'):
|
||||
confirm = 'y'
|
||||
else:
|
||||
confirm = input(f"{COLOR_PROMPT}Proceed with installation using these settings? (y/N): {COLOR_RESET}").strip().lower()
|
||||
if confirm != 'y':
|
||||
self.logger.info("User cancelled at final confirmation.")
|
||||
print(f"{COLOR_INFO}Installation cancelled by user.{COLOR_RESET}")
|
||||
return None
|
||||
|
||||
self.logger.info("Discovery phase complete.")
|
||||
context_for_logging = self.context.copy()
|
||||
if 'nexus_api_key' in context_for_logging and context_for_logging['nexus_api_key'] is not None:
|
||||
context_for_logging['nexus_api_key'] = "[REDACTED]"
|
||||
self.logger.info(f"Context: {context_for_logging}")
|
||||
return self.context
|
||||
67
jackify/backend/core/modlist_operations_game_detection.py
Normal file
@@ -0,0 +1,67 @@
|
||||
"""Game detection methods for ModlistInstallCLI (Mixin)."""
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ModlistOperationsGameDetectionMixin:
|
||||
"""Mixin providing game type detection methods."""
|
||||
|
||||
def detect_game_type(self, modlist_info: Optional[Dict] = None, wabbajack_file_path: Optional[Path] = None) -> Optional[str]:
|
||||
"""
|
||||
Detect the game type for a modlist installation.
|
||||
|
||||
Args:
|
||||
modlist_info: Dictionary containing modlist information (for online modlists)
|
||||
wabbajack_file_path: Path to .wabbajack file (for local files)
|
||||
|
||||
Returns:
|
||||
Jackify game type string or None if detection fails
|
||||
"""
|
||||
if wabbajack_file_path:
|
||||
self.logger.info(f"Detecting game type from .wabbajack file: {wabbajack_file_path}")
|
||||
game_type = self.wabbajack_parser.parse_wabbajack_game_type(wabbajack_file_path)
|
||||
if game_type:
|
||||
self.logger.info(f"Detected game type from .wabbajack file: {game_type}")
|
||||
return game_type
|
||||
else:
|
||||
self.logger.warning(f"Could not detect game type from .wabbajack file: {wabbajack_file_path}")
|
||||
return None
|
||||
elif modlist_info and 'game' in modlist_info:
|
||||
game_name = modlist_info['game'].lower()
|
||||
self.logger.info(f"Detecting game type from modlist info: {game_name}")
|
||||
|
||||
game_mapping = {
|
||||
'skyrim special edition': 'skyrim',
|
||||
'skyrim': 'skyrim',
|
||||
'fallout 4': 'fallout4',
|
||||
'fallout new vegas': 'falloutnv',
|
||||
'oblivion': 'oblivion',
|
||||
'starfield': 'starfield',
|
||||
'oblivion remastered': 'oblivion_remastered'
|
||||
}
|
||||
|
||||
game_type = game_mapping.get(game_name)
|
||||
if game_type:
|
||||
self.logger.info(f"Mapped game name '{game_name}' to game type: {game_type}")
|
||||
return game_type
|
||||
else:
|
||||
self.logger.warning(f"Unknown game name in modlist info: {game_name}")
|
||||
return None
|
||||
else:
|
||||
self.logger.warning("No modlist info or .wabbajack file path provided for game detection")
|
||||
return None
|
||||
|
||||
def check_game_support(self, game_type: str) -> bool:
|
||||
"""
|
||||
Check if a game type is supported by Jackify's post-install configuration.
|
||||
|
||||
Args:
|
||||
game_type: Jackify game type string
|
||||
|
||||
Returns:
|
||||
True if the game is supported, False otherwise
|
||||
"""
|
||||
return self.wabbajack_parser.is_supported_game(game_type)
|
||||
99
jackify/backend/core/modlist_operations_nexus.py
Normal file
@@ -0,0 +1,99 @@
|
||||
"""Nexus and engine methods for ModlistInstallCLI (Mixin)."""
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from ..handlers.ui_colors import COLOR_ERROR, COLOR_INFO, COLOR_RESET
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ModlistOperationsNexusMixin:
|
||||
"""Mixin providing Nexus API and engine methods."""
|
||||
|
||||
def _get_nexus_api_key(self) -> Optional[str]:
|
||||
return self.context.get('nexus_api_key')
|
||||
|
||||
def get_all_modlists_from_engine(self, game_type=None):
|
||||
"""
|
||||
Call the Jackify engine with 'list-modlists' and return a list of modlist dicts.
|
||||
Each dict should have at least 'id', 'game', 'download_size', 'install_size', 'total_size', and status flags.
|
||||
|
||||
Args:
|
||||
game_type (str, optional): Filter by game type (e.g., "Skyrim", "Fallout New Vegas")
|
||||
"""
|
||||
from .modlist_operations import get_jackify_engine_path
|
||||
|
||||
engine_executable = get_jackify_engine_path()
|
||||
engine_dir = os.path.dirname(engine_executable)
|
||||
if not os.path.exists(engine_executable):
|
||||
print(f"{COLOR_ERROR}Error: jackify-install-engine not found at expected location.{COLOR_RESET}")
|
||||
print(f"{COLOR_INFO}Expected: {engine_executable}{COLOR_RESET}")
|
||||
return []
|
||||
env = os.environ.copy()
|
||||
env["DOTNET_SYSTEM_GLOBALIZATION_INVARIANT"] = "1"
|
||||
command = [engine_executable, 'list-modlists', '--show-all-sizes', '--show-machine-url']
|
||||
|
||||
if game_type:
|
||||
command.extend(['--game', game_type])
|
||||
try:
|
||||
result = subprocess.run(
|
||||
command,
|
||||
capture_output=True, text=True, check=True,
|
||||
env=env, cwd=engine_dir
|
||||
)
|
||||
lines = result.stdout.splitlines()
|
||||
modlists = []
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if not line or line.startswith('Loading') or line.startswith('Loaded'):
|
||||
continue
|
||||
|
||||
status_down = '[DOWN]' in line
|
||||
status_nsfw = '[NSFW]' in line
|
||||
clean_line = line.replace('[DOWN]', '').replace('[NSFW]', '').strip()
|
||||
parts = clean_line.rsplit(' - ', 3)
|
||||
if len(parts) != 4:
|
||||
continue
|
||||
|
||||
modlist_name = parts[0].strip()
|
||||
game_name = parts[1].strip()
|
||||
sizes_str = parts[2].strip()
|
||||
machine_url = parts[3].strip()
|
||||
size_parts = sizes_str.split('|')
|
||||
if len(size_parts) != 3:
|
||||
continue
|
||||
|
||||
download_size = size_parts[0].strip()
|
||||
install_size = size_parts[1].strip()
|
||||
total_size = size_parts[2].strip()
|
||||
if not modlist_name or not game_name or not machine_url:
|
||||
continue
|
||||
|
||||
modlists.append({
|
||||
'id': modlist_name,
|
||||
'name': modlist_name,
|
||||
'game': game_name,
|
||||
'download_size': download_size,
|
||||
'install_size': install_size,
|
||||
'total_size': total_size,
|
||||
'machine_url': machine_url,
|
||||
'status_down': status_down,
|
||||
'status_nsfw': status_nsfw
|
||||
})
|
||||
return modlists
|
||||
except subprocess.CalledProcessError as e:
|
||||
self.logger.error(f"list-modlists failed. Code: {e.returncode}")
|
||||
if e.stdout:
|
||||
self.logger.error(f"Engine stdout:\n{e.stdout}")
|
||||
if e.stderr:
|
||||
self.logger.error(f"Engine stderr:\n{e.stderr}")
|
||||
print(f"{COLOR_ERROR}Failed to fetch modlist list. Engine error (Code: {e.returncode}).{COLOR_ERROR}")
|
||||
return []
|
||||
except Exception as e:
|
||||
self.logger.error(f"Unexpected error fetching modlists: {e}", exc_info=True)
|
||||
print(f"{COLOR_ERROR}Unexpected error fetching modlists: {e}{COLOR_ERROR}")
|
||||
return []
|
||||
3
jackify/backend/data/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
Data package for static configuration and reference data.
|
||||
"""
|
||||
46
jackify/backend/data/ttw_compatible_modlists.py
Normal file
@@ -0,0 +1,46 @@
|
||||
"""
|
||||
TTW-Compatible Modlists Configuration
|
||||
|
||||
Defines which Fallout New Vegas modlists support Tale of Two Wastelands.
|
||||
This whitelist determines when Jackify should offer TTW installation after
|
||||
a successful modlist installation.
|
||||
"""
|
||||
|
||||
TTW_COMPATIBLE_MODLISTS = {
|
||||
# Exact modlist names that support/require TTW
|
||||
"exact_matches": [
|
||||
"Begin Again",
|
||||
"Uranium Fever",
|
||||
"The Badlands",
|
||||
"Wild Card TTW",
|
||||
],
|
||||
|
||||
# Pattern matching for modlist names (regex)
|
||||
"patterns": [
|
||||
r".*TTW.*", # Any modlist with TTW in name
|
||||
r".*Tale.*Two.*Wastelands.*",
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def is_ttw_compatible(modlist_name: str) -> bool:
|
||||
"""Check if modlist name matches TTW compatibility criteria
|
||||
|
||||
Args:
|
||||
modlist_name: Name of the modlist to check
|
||||
|
||||
Returns:
|
||||
bool: True if modlist is TTW-compatible, False otherwise
|
||||
"""
|
||||
import re
|
||||
|
||||
# Check exact matches
|
||||
if modlist_name in TTW_COMPATIBLE_MODLISTS['exact_matches']:
|
||||
return True
|
||||
|
||||
# Check pattern matches
|
||||
for pattern in TTW_COMPATIBLE_MODLISTS['patterns']:
|
||||
if re.match(pattern, modlist_name, re.IGNORECASE):
|
||||
return True
|
||||
|
||||
return False
|
||||
@@ -5,17 +5,10 @@ Reusable tab completion functions for Jackify CLI, including bash-like path comp
|
||||
|
||||
import os
|
||||
import readline
|
||||
import logging # Added for debugging
|
||||
import logging
|
||||
|
||||
# Get a logger for this module
|
||||
completer_logger = logging.getLogger(__name__) # Logger will be named src.modules.completers
|
||||
|
||||
# Set level to DEBUG for this logger to ensure all debug messages are generated.
|
||||
# These messages will be handled by handlers configured in the main application (e.g., via LoggingHandler).
|
||||
completer_logger = logging.getLogger(__name__)
|
||||
completer_logger.setLevel(logging.INFO)
|
||||
|
||||
# Ensure messages DO NOT propagate to the root logger's console handler by default.
|
||||
# A dedicated file handler will be added in jackify-cli.py.
|
||||
completer_logger.propagate = False
|
||||
|
||||
# IMPORTANT: Do NOT include '/' in the completer delimiters!
|
||||
@@ -68,7 +61,6 @@ def path_completer(text, state):
|
||||
|
||||
final_match_strings_for_readline = []
|
||||
text_dir_part = os.path.dirname(text)
|
||||
# If text is a directory with trailing slash, use it as the base for completions
|
||||
if os.path.isdir(text) and text.endswith(os.sep):
|
||||
base_path = text
|
||||
elif os.path.isdir(text):
|
||||
|
||||