From 35569145603b79aa5ea38810bb2d59594ef6370e Mon Sep 17 00:00:00 2001 From: Omni Date: Fri, 13 Mar 2026 14:43:25 +0000 Subject: [PATCH] Sync from development - prepare for v0.5.0 --- CHANGELOG.md | 51 ++ README.md | 2 +- .../AdditionalTools/protonplus-ge-install.png | Bin .../AdditionalTools/protonplus-main.png | Bin .../protonupqt-add-version.png | Bin .../AdditionalTools/protonupqt-main.png | Bin .../steam-compatibility-ge-proton.png | Bin .../jackify-configure-existing-blank.png | Bin ...kify-configure-existing-select-modlist.png | Bin ...ckify-modlist-tasks-configure-existing.png | Bin .../jackify-configure-new-blank.png | Bin .../jackify-configure-new-browse-mo2.png | Bin .../jackify-configure-new-filled.png | Bin .../shared/AddNonSteamGame.png | Bin .../shared/ClearRootBuilder.png | Bin .../shared/Jackify_Github_Banner.png | Bin .../shared/MO2DownloadsError.png | Bin .../shared/MO2RegisterNXMLinks.png | Bin .../shared/MO2_Executables_Cog.png | Bin .../shared/ProtonTricks_GUI_winecfg.png | Bin .../ProtonTricks_wincfg_select_default.png | Bin .../shared/ProtontricksDiscover.png | Bin .../shared/ProtontricksDiscoverInstall.png | Bin .../shared/Protontricks_GUI-dotfiles.png | Bin .../shared/ReEnableENBMods.png | Bin .../shared/SSEDisplayEditINI.png | Bin .../shared/SSEDisplayEditResolution.png | Bin .../shared/STEAM_COMPAT_MOUNTS.png | Bin .../shared/Shared-AddMO2NonSteamGame.png | Bin .../shared/Shared-AddNonSteamGame.png | Bin .../shared/Shared-BrowseNonSteamGame.png | Bin .../shared/Shared-MO2DownloadsError.png | Bin .../shared/Shared-MO2ExecutablesCog.png | Bin .../shared/Shared-MO2PortableError.png | Bin .../shared/Shared-MO2RegisterNXMLinks.png | Bin .../shared/Shared-ProtonNonSteamGame.png | Bin .../shared/Shared-ProtonUpQT-AddVersion.png | Bin .../Shared-ProtonUpQT-InstallVersion.png | Bin .../Shared-ProtonUpQTDiscoverSearch.png | Bin .../shared/Shared-ProtontricksDiscover.png | Bin .../Shared-ProtontricksDiscoverInstall.png | Bin .../shared/Shared-STEAM_COMPAT_MOUNTS.png | Bin .../shared/Shared-VCRedistInstallComplete.png | Bin .../shared/Shared-VCRedistInstallStart.png | Bin .../shared/Shared-WineShell.png | Bin .../shared/Shared/Jackify_Github_Banner.png | Bin .../shared/Shared/mo2-run-button.png | Bin .../Shared/nexus-jackify-download-page.png | Bin .../Shared/steam-library-tuxborn-premium.png | Bin .../shared/Shared_ProtonUp-AddVersion.png | Bin .../shared/Shared_ProtonUp-QTSearch.png | Bin .../Shared_ProtonUp-QTsteamtinkerlaunch.png | Bin .../shared/Shared_STL-CustomCommand.png | Bin .../shared/VCRedistInstallComplete.png | Bin .../shared/VCRedistInstallStart.png | Bin .../shared/WineShell.png | Bin .../shared/mo2-run-button.png | Bin .../shared/nexus-jackify-download-page.png | Bin .../shared/start.png | 0 .../shared/steam-library-tuxborn-premium.png | Bin jackify/__init__.py | 2 +- jackify/backend/core/modlist_operations.py | 88 ++- .../modlist_operations_configuration_cli.py | 197 +++-- .../modlist_operations_configuration_gui.py | 4 +- .../core/modlist_operations_discovery.py | 40 + jackify/backend/handlers/config_handler.py | 57 +- .../backend/handlers/config_handler_proton.py | 101 +++ .../backend/handlers/menu_handler_modlist.py | 172 +++-- .../backend/handlers/modlist_configuration.py | 18 +- .../handlers/modlist_install_cli_ttw.py | 7 + jackify/backend/handlers/path_handler_mo2.py | 89 +++ .../backend/handlers/path_handler_steam.py | 36 +- jackify/backend/handlers/subprocess_utils.py | 67 +- .../backend/handlers/ttw_installer_backend.py | 24 + .../services/automated_prefix_service.py | 7 +- .../services/automated_prefix_workflow.py | 112 +-- .../services/download_watcher_service.py | 138 ++++ .../services/file_validator_service.py | 261 +++++++ .../services/manual_download_manager.py | 124 ++++ .../manual_download_manager_api_mixin.py | 163 +++++ .../manual_download_manager_runtime_mixin.py | 479 ++++++++++++ jackify/backend/services/mo2_setup_service.py | 95 ++- jackify/backend/services/modlist_service.py | 6 +- .../services/modlist_service_installation.py | 86 ++- .../backend/services/native_steam_service.py | 18 +- .../backend/services/nexus_oauth_protocol.py | 44 +- .../backend/services/steam_restart_service.py | 1 + .../backend/services/ttw_installer_service.py | 62 ++ jackify/backend/services/update_service.py | 77 +- .../services/vnv_post_install_service.py | 257 +++++-- .../services/wabbajack_installer_service.py | 97 +-- jackify/backend/utils/cc_content_detector.py | 34 + jackify/backend/utils/engine_error_parser.py | 24 + jackify/engine/Wabbajack.CLI.Builder.dll | Bin 22016 -> 24576 bytes jackify/engine/Wabbajack.Common.dll | Bin 205312 -> 205312 bytes jackify/engine/Wabbajack.Compiler.dll | Bin 160256 -> 160256 bytes jackify/engine/Wabbajack.Compression.BSA.dll | Bin 94720 -> 94720 bytes jackify/engine/Wabbajack.Compression.Zip.dll | Bin 18944 -> 18944 bytes jackify/engine/Wabbajack.Configuration.dll | Bin 4096 -> 4096 bytes jackify/engine/Wabbajack.DTOs.dll | Bin 142336 -> 142336 bytes .../engine/Wabbajack.Downloaders.Bethesda.dll | Bin 18432 -> 18432 bytes .../Wabbajack.Downloaders.Dispatcher.dll | Bin 29184 -> 29184 bytes .../engine/Wabbajack.Downloaders.GameFile.dll | Bin 16384 -> 16384 bytes .../Wabbajack.Downloaders.GoogleDrive.dll | Bin 17920 -> 17920 bytes jackify/engine/Wabbajack.Downloaders.Http.dll | Bin 15872 -> 15872 bytes ...ajack.Downloaders.IPS4OAuth2Downloader.dll | Bin 35328 -> 35328 bytes .../Wabbajack.Downloaders.Interfaces.dll | Bin 7168 -> 7168 bytes .../engine/Wabbajack.Downloaders.Manual.dll | Bin 9216 -> 8192 bytes .../Wabbajack.Downloaders.MediaFire.dll | Bin 15872 -> 15872 bytes jackify/engine/Wabbajack.Downloaders.Mega.dll | Bin 16384 -> 16384 bytes .../engine/Wabbajack.Downloaders.ModDB.dll | Bin 19456 -> 19456 bytes .../engine/Wabbajack.Downloaders.Nexus.dll | Bin 23552 -> 16896 bytes ...abbajack.Downloaders.VerificationCache.dll | Bin 13824 -> 13824 bytes .../Wabbajack.Downloaders.WabbajackCDN.dll | Bin 24576 -> 24576 bytes jackify/engine/Wabbajack.FileExtractor.dll | Bin 79872 -> 79872 bytes jackify/engine/Wabbajack.Hashing.PHash.dll | Bin 50176 -> 50176 bytes jackify/engine/Wabbajack.Hashing.xxHash64.dll | Bin 21504 -> 21504 bytes jackify/engine/Wabbajack.IO.Async.dll | Bin 15360 -> 15360 bytes jackify/engine/Wabbajack.Installer.dll | Bin 136704 -> 142848 bytes .../Wabbajack.Networking.BethesdaNet.dll | Bin 39936 -> 39936 bytes .../engine/Wabbajack.Networking.Discord.dll | Bin 14336 -> 14336 bytes .../engine/Wabbajack.Networking.GitHub.dll | Bin 21504 -> 21504 bytes .../Wabbajack.Networking.Http.Interfaces.dll | Bin 5632 -> 5632 bytes jackify/engine/Wabbajack.Networking.Http.dll | Bin 37888 -> 37888 bytes .../engine/Wabbajack.Networking.NexusApi.dll | Bin 80896 -> 82432 bytes ...abbajack.Networking.WabbajackClientApi.dll | Bin 77824 -> 77824 bytes jackify/engine/Wabbajack.Paths.IO.dll | Bin 34816 -> 34816 bytes jackify/engine/Wabbajack.Paths.dll | Bin 17408 -> 17408 bytes jackify/engine/Wabbajack.RateLimiter.dll | Bin 24576 -> 24576 bytes jackify/engine/Wabbajack.Server.Lib.dll | Bin 6656 -> 6656 bytes .../Wabbajack.Services.OSIntegrated.dll | Bin 55808 -> 55808 bytes jackify/engine/Wabbajack.VFS.Interfaces.dll | Bin 5120 -> 5120 bytes jackify/engine/Wabbajack.VFS.dll | Bin 64512 -> 64512 bytes jackify/engine/jackify-engine.deps.json | 451 ++++++------ jackify/engine/jackify-engine.dll | Bin 230912 -> 232448 bytes .../cli/commands/manual_download_flow.py | 479 ++++++++++++ .../cli/commands/vnv_manual_downloads.py | 140 ++++ .../frontends/cli/menus/additional_menu.py | 91 ++- .../frontends/cli/ui/indeterminate_status.py | 70 ++ .../gui/dialogs/existing_setup_dialog.py | 204 ++++++ .../gui/dialogs/manual_download_dialog.py | 466 ++++++++++++ .../frontends/gui/dialogs/settings_dialog.py | 16 +- .../frontends/gui/dialogs/success_dialog.py | 5 +- .../gui/mixins/main_window_geometry.py | 6 + .../frontends/gui/mixins/main_window_ui.py | 4 + .../gui/screens/configure_existing_modlist.py | 49 +- .../screens/configure_existing_modlist_ui.py | 7 +- .../configure_existing_modlist_workflow.py | 136 ++-- .../gui/screens/configure_new_modlist.py | 49 +- .../screens/configure_new_modlist_console.py | 10 +- .../screens/configure_new_modlist_dialogs.py | 302 +++----- .../screens/configure_new_modlist_ui_setup.py | 7 +- .../screens/configure_new_modlist_workflow.py | 25 +- .../gui/screens/install_mo2_screen.py | 118 ++- .../frontends/gui/screens/install_modlist.py | 42 +- .../screens/install_modlist_configuration.py | 53 +- .../gui/screens/install_modlist_console.py | 43 -- .../install_modlist_installer_thread.py | 234 +++++- .../gui/screens/install_modlist_nexus.py | 230 ++++-- .../screens/install_modlist_postinstall.py | 21 + .../gui/screens/install_modlist_progress.py | 150 +++- .../install_modlist_shortcut_dialog.py | 154 ++-- .../gui/screens/install_modlist_ui_setup.py | 6 +- .../gui/screens/install_modlist_vnv.py | 178 +---- .../gui/screens/install_modlist_workflow.py | 688 +++++++++--------- .../install_modlist_workflow_execution.py | 516 +++++++++++++ jackify/frontends/gui/screens/install_ttw.py | 39 +- .../gui/screens/install_ttw_output.py | 3 + .../gui/screens/install_ttw_thread.py | 6 +- .../gui/screens/install_ttw_workflow.py | 16 +- .../gui/screens/screen_back_mixin.py | 46 ++ .../gui/screens/screen_focus_reclaim.py | 58 ++ .../gui/screens/wabbajack_installer.py | 69 +- .../gui/services/vnv_automation_controller.py | 402 ++++++++++ .../gui/widgets/file_progress_list.py | 4 +- jackify/shared/appimage_utils.py | 4 +- jackify/shared/errors.py | 68 +- jackify/shared/steam_utils.py | 222 +++++- requirements.txt | 5 +- 179 files changed, 7126 insertions(+), 1806 deletions(-) rename assets/images/wiki/{ModlistGuides => UserGuide}/AdditionalTools/protonplus-ge-install.png (100%) rename assets/images/wiki/{ModlistGuides => UserGuide}/AdditionalTools/protonplus-main.png (100%) rename assets/images/wiki/{ModlistGuides => UserGuide}/AdditionalTools/protonupqt-add-version.png (100%) rename assets/images/wiki/{ModlistGuides => UserGuide}/AdditionalTools/protonupqt-main.png (100%) rename assets/images/wiki/{ModlistGuides => UserGuide}/AdditionalTools/steam-compatibility-ge-proton.png (100%) rename assets/images/wiki/{ModlistGuides => UserGuide}/ConfigureExisting/jackify-configure-existing-blank.png (100%) rename assets/images/wiki/{ModlistGuides => UserGuide}/ConfigureExisting/jackify-configure-existing-select-modlist.png (100%) rename assets/images/wiki/{ModlistGuides => UserGuide}/ConfigureExisting/jackify-modlist-tasks-configure-existing.png (100%) rename assets/images/wiki/{ModlistGuides => UserGuide}/ConfigureNew/jackify-configure-new-blank.png (100%) rename assets/images/wiki/{ModlistGuides => UserGuide}/ConfigureNew/jackify-configure-new-browse-mo2.png (100%) rename assets/images/wiki/{ModlistGuides => UserGuide}/ConfigureNew/jackify-configure-new-filled.png (100%) rename assets/images/wiki/{ModlistGuides => UserGuide}/shared/AddNonSteamGame.png (100%) rename assets/images/wiki/{ModlistGuides => UserGuide}/shared/ClearRootBuilder.png (100%) rename assets/images/wiki/{ModlistGuides => UserGuide}/shared/Jackify_Github_Banner.png (100%) rename assets/images/wiki/{ModlistGuides => UserGuide}/shared/MO2DownloadsError.png (100%) rename assets/images/wiki/{ModlistGuides => UserGuide}/shared/MO2RegisterNXMLinks.png (100%) rename assets/images/wiki/{ModlistGuides => UserGuide}/shared/MO2_Executables_Cog.png (100%) rename assets/images/wiki/{ModlistGuides => UserGuide}/shared/ProtonTricks_GUI_winecfg.png (100%) rename assets/images/wiki/{ModlistGuides => UserGuide}/shared/ProtonTricks_wincfg_select_default.png (100%) rename assets/images/wiki/{ModlistGuides => UserGuide}/shared/ProtontricksDiscover.png (100%) rename assets/images/wiki/{ModlistGuides => UserGuide}/shared/ProtontricksDiscoverInstall.png (100%) rename assets/images/wiki/{ModlistGuides => UserGuide}/shared/Protontricks_GUI-dotfiles.png (100%) rename assets/images/wiki/{ModlistGuides => UserGuide}/shared/ReEnableENBMods.png (100%) rename assets/images/wiki/{ModlistGuides => UserGuide}/shared/SSEDisplayEditINI.png (100%) rename assets/images/wiki/{ModlistGuides => UserGuide}/shared/SSEDisplayEditResolution.png (100%) rename assets/images/wiki/{ModlistGuides => UserGuide}/shared/STEAM_COMPAT_MOUNTS.png (100%) rename assets/images/wiki/{ModlistGuides => UserGuide}/shared/Shared-AddMO2NonSteamGame.png (100%) rename assets/images/wiki/{ModlistGuides => UserGuide}/shared/Shared-AddNonSteamGame.png (100%) rename assets/images/wiki/{ModlistGuides => UserGuide}/shared/Shared-BrowseNonSteamGame.png (100%) rename assets/images/wiki/{ModlistGuides => UserGuide}/shared/Shared-MO2DownloadsError.png (100%) rename assets/images/wiki/{ModlistGuides => UserGuide}/shared/Shared-MO2ExecutablesCog.png (100%) rename assets/images/wiki/{ModlistGuides => UserGuide}/shared/Shared-MO2PortableError.png (100%) rename assets/images/wiki/{ModlistGuides => UserGuide}/shared/Shared-MO2RegisterNXMLinks.png (100%) rename assets/images/wiki/{ModlistGuides => UserGuide}/shared/Shared-ProtonNonSteamGame.png (100%) rename assets/images/wiki/{ModlistGuides => UserGuide}/shared/Shared-ProtonUpQT-AddVersion.png (100%) rename assets/images/wiki/{ModlistGuides => UserGuide}/shared/Shared-ProtonUpQT-InstallVersion.png (100%) rename assets/images/wiki/{ModlistGuides => UserGuide}/shared/Shared-ProtonUpQTDiscoverSearch.png (100%) rename assets/images/wiki/{ModlistGuides => UserGuide}/shared/Shared-ProtontricksDiscover.png (100%) rename assets/images/wiki/{ModlistGuides => UserGuide}/shared/Shared-ProtontricksDiscoverInstall.png (100%) rename assets/images/wiki/{ModlistGuides => UserGuide}/shared/Shared-STEAM_COMPAT_MOUNTS.png (100%) rename assets/images/wiki/{ModlistGuides => UserGuide}/shared/Shared-VCRedistInstallComplete.png (100%) rename assets/images/wiki/{ModlistGuides => UserGuide}/shared/Shared-VCRedistInstallStart.png (100%) rename assets/images/wiki/{ModlistGuides => UserGuide}/shared/Shared-WineShell.png (100%) rename assets/images/wiki/{ModlistGuides => UserGuide}/shared/Shared/Jackify_Github_Banner.png (100%) rename assets/images/wiki/{ModlistGuides => UserGuide}/shared/Shared/mo2-run-button.png (100%) rename assets/images/wiki/{ModlistGuides => UserGuide}/shared/Shared/nexus-jackify-download-page.png (100%) rename assets/images/wiki/{ModlistGuides => UserGuide}/shared/Shared/steam-library-tuxborn-premium.png (100%) rename assets/images/wiki/{ModlistGuides => UserGuide}/shared/Shared_ProtonUp-AddVersion.png (100%) rename assets/images/wiki/{ModlistGuides => UserGuide}/shared/Shared_ProtonUp-QTSearch.png (100%) rename assets/images/wiki/{ModlistGuides => UserGuide}/shared/Shared_ProtonUp-QTsteamtinkerlaunch.png (100%) rename assets/images/wiki/{ModlistGuides => UserGuide}/shared/Shared_STL-CustomCommand.png (100%) rename assets/images/wiki/{ModlistGuides => UserGuide}/shared/VCRedistInstallComplete.png (100%) rename assets/images/wiki/{ModlistGuides => UserGuide}/shared/VCRedistInstallStart.png (100%) rename assets/images/wiki/{ModlistGuides => UserGuide}/shared/WineShell.png (100%) rename assets/images/wiki/{ModlistGuides => UserGuide}/shared/mo2-run-button.png (100%) rename assets/images/wiki/{ModlistGuides => UserGuide}/shared/nexus-jackify-download-page.png (100%) rename assets/images/wiki/{ModlistGuides => UserGuide}/shared/start.png (100%) rename assets/images/wiki/{ModlistGuides => UserGuide}/shared/steam-library-tuxborn-premium.png (100%) create mode 100644 jackify/backend/services/download_watcher_service.py create mode 100644 jackify/backend/services/file_validator_service.py create mode 100644 jackify/backend/services/manual_download_manager.py create mode 100644 jackify/backend/services/manual_download_manager_api_mixin.py create mode 100644 jackify/backend/services/manual_download_manager_runtime_mixin.py create mode 100644 jackify/backend/services/ttw_installer_service.py create mode 100644 jackify/backend/utils/cc_content_detector.py create mode 100644 jackify/frontends/cli/commands/manual_download_flow.py create mode 100644 jackify/frontends/cli/commands/vnv_manual_downloads.py create mode 100644 jackify/frontends/cli/ui/indeterminate_status.py create mode 100644 jackify/frontends/gui/dialogs/existing_setup_dialog.py create mode 100644 jackify/frontends/gui/dialogs/manual_download_dialog.py create mode 100644 jackify/frontends/gui/screens/install_modlist_workflow_execution.py create mode 100644 jackify/frontends/gui/screens/screen_focus_reclaim.py create mode 100644 jackify/frontends/gui/services/vnv_automation_controller.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 6817690..d73f617 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,56 @@ # Jackify Changelog +## 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 diff --git a/README.md b/README.md index 1cba679..2464210 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ Jackify is a Linux application for installing and configuring Wabbajack modlists - Non-Premium users can still install modlists via Wabbajack under Proton - Native non-premium support planned for a future release - See the [User Guide](https://github.com/Omni-guides/Jackify/wiki/User-Guide) for full details on the options available -- **FUSE** (required for AppImage execution, pre-installed on most distributions) +- **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` diff --git a/assets/images/wiki/ModlistGuides/AdditionalTools/protonplus-ge-install.png b/assets/images/wiki/UserGuide/AdditionalTools/protonplus-ge-install.png similarity index 100% rename from assets/images/wiki/ModlistGuides/AdditionalTools/protonplus-ge-install.png rename to assets/images/wiki/UserGuide/AdditionalTools/protonplus-ge-install.png diff --git a/assets/images/wiki/ModlistGuides/AdditionalTools/protonplus-main.png b/assets/images/wiki/UserGuide/AdditionalTools/protonplus-main.png similarity index 100% rename from assets/images/wiki/ModlistGuides/AdditionalTools/protonplus-main.png rename to assets/images/wiki/UserGuide/AdditionalTools/protonplus-main.png diff --git a/assets/images/wiki/ModlistGuides/AdditionalTools/protonupqt-add-version.png b/assets/images/wiki/UserGuide/AdditionalTools/protonupqt-add-version.png similarity index 100% rename from assets/images/wiki/ModlistGuides/AdditionalTools/protonupqt-add-version.png rename to assets/images/wiki/UserGuide/AdditionalTools/protonupqt-add-version.png diff --git a/assets/images/wiki/ModlistGuides/AdditionalTools/protonupqt-main.png b/assets/images/wiki/UserGuide/AdditionalTools/protonupqt-main.png similarity index 100% rename from assets/images/wiki/ModlistGuides/AdditionalTools/protonupqt-main.png rename to assets/images/wiki/UserGuide/AdditionalTools/protonupqt-main.png diff --git a/assets/images/wiki/ModlistGuides/AdditionalTools/steam-compatibility-ge-proton.png b/assets/images/wiki/UserGuide/AdditionalTools/steam-compatibility-ge-proton.png similarity index 100% rename from assets/images/wiki/ModlistGuides/AdditionalTools/steam-compatibility-ge-proton.png rename to assets/images/wiki/UserGuide/AdditionalTools/steam-compatibility-ge-proton.png diff --git a/assets/images/wiki/ModlistGuides/ConfigureExisting/jackify-configure-existing-blank.png b/assets/images/wiki/UserGuide/ConfigureExisting/jackify-configure-existing-blank.png similarity index 100% rename from assets/images/wiki/ModlistGuides/ConfigureExisting/jackify-configure-existing-blank.png rename to assets/images/wiki/UserGuide/ConfigureExisting/jackify-configure-existing-blank.png diff --git a/assets/images/wiki/ModlistGuides/ConfigureExisting/jackify-configure-existing-select-modlist.png b/assets/images/wiki/UserGuide/ConfigureExisting/jackify-configure-existing-select-modlist.png similarity index 100% rename from assets/images/wiki/ModlistGuides/ConfigureExisting/jackify-configure-existing-select-modlist.png rename to assets/images/wiki/UserGuide/ConfigureExisting/jackify-configure-existing-select-modlist.png diff --git a/assets/images/wiki/ModlistGuides/ConfigureExisting/jackify-modlist-tasks-configure-existing.png b/assets/images/wiki/UserGuide/ConfigureExisting/jackify-modlist-tasks-configure-existing.png similarity index 100% rename from assets/images/wiki/ModlistGuides/ConfigureExisting/jackify-modlist-tasks-configure-existing.png rename to assets/images/wiki/UserGuide/ConfigureExisting/jackify-modlist-tasks-configure-existing.png diff --git a/assets/images/wiki/ModlistGuides/ConfigureNew/jackify-configure-new-blank.png b/assets/images/wiki/UserGuide/ConfigureNew/jackify-configure-new-blank.png similarity index 100% rename from assets/images/wiki/ModlistGuides/ConfigureNew/jackify-configure-new-blank.png rename to assets/images/wiki/UserGuide/ConfigureNew/jackify-configure-new-blank.png diff --git a/assets/images/wiki/ModlistGuides/ConfigureNew/jackify-configure-new-browse-mo2.png b/assets/images/wiki/UserGuide/ConfigureNew/jackify-configure-new-browse-mo2.png similarity index 100% rename from assets/images/wiki/ModlistGuides/ConfigureNew/jackify-configure-new-browse-mo2.png rename to assets/images/wiki/UserGuide/ConfigureNew/jackify-configure-new-browse-mo2.png diff --git a/assets/images/wiki/ModlistGuides/ConfigureNew/jackify-configure-new-filled.png b/assets/images/wiki/UserGuide/ConfigureNew/jackify-configure-new-filled.png similarity index 100% rename from assets/images/wiki/ModlistGuides/ConfigureNew/jackify-configure-new-filled.png rename to assets/images/wiki/UserGuide/ConfigureNew/jackify-configure-new-filled.png diff --git a/assets/images/wiki/ModlistGuides/shared/AddNonSteamGame.png b/assets/images/wiki/UserGuide/shared/AddNonSteamGame.png similarity index 100% rename from assets/images/wiki/ModlistGuides/shared/AddNonSteamGame.png rename to assets/images/wiki/UserGuide/shared/AddNonSteamGame.png diff --git a/assets/images/wiki/ModlistGuides/shared/ClearRootBuilder.png b/assets/images/wiki/UserGuide/shared/ClearRootBuilder.png similarity index 100% rename from assets/images/wiki/ModlistGuides/shared/ClearRootBuilder.png rename to assets/images/wiki/UserGuide/shared/ClearRootBuilder.png diff --git a/assets/images/wiki/ModlistGuides/shared/Jackify_Github_Banner.png b/assets/images/wiki/UserGuide/shared/Jackify_Github_Banner.png similarity index 100% rename from assets/images/wiki/ModlistGuides/shared/Jackify_Github_Banner.png rename to assets/images/wiki/UserGuide/shared/Jackify_Github_Banner.png diff --git a/assets/images/wiki/ModlistGuides/shared/MO2DownloadsError.png b/assets/images/wiki/UserGuide/shared/MO2DownloadsError.png similarity index 100% rename from assets/images/wiki/ModlistGuides/shared/MO2DownloadsError.png rename to assets/images/wiki/UserGuide/shared/MO2DownloadsError.png diff --git a/assets/images/wiki/ModlistGuides/shared/MO2RegisterNXMLinks.png b/assets/images/wiki/UserGuide/shared/MO2RegisterNXMLinks.png similarity index 100% rename from assets/images/wiki/ModlistGuides/shared/MO2RegisterNXMLinks.png rename to assets/images/wiki/UserGuide/shared/MO2RegisterNXMLinks.png diff --git a/assets/images/wiki/ModlistGuides/shared/MO2_Executables_Cog.png b/assets/images/wiki/UserGuide/shared/MO2_Executables_Cog.png similarity index 100% rename from assets/images/wiki/ModlistGuides/shared/MO2_Executables_Cog.png rename to assets/images/wiki/UserGuide/shared/MO2_Executables_Cog.png diff --git a/assets/images/wiki/ModlistGuides/shared/ProtonTricks_GUI_winecfg.png b/assets/images/wiki/UserGuide/shared/ProtonTricks_GUI_winecfg.png similarity index 100% rename from assets/images/wiki/ModlistGuides/shared/ProtonTricks_GUI_winecfg.png rename to assets/images/wiki/UserGuide/shared/ProtonTricks_GUI_winecfg.png diff --git a/assets/images/wiki/ModlistGuides/shared/ProtonTricks_wincfg_select_default.png b/assets/images/wiki/UserGuide/shared/ProtonTricks_wincfg_select_default.png similarity index 100% rename from assets/images/wiki/ModlistGuides/shared/ProtonTricks_wincfg_select_default.png rename to assets/images/wiki/UserGuide/shared/ProtonTricks_wincfg_select_default.png diff --git a/assets/images/wiki/ModlistGuides/shared/ProtontricksDiscover.png b/assets/images/wiki/UserGuide/shared/ProtontricksDiscover.png similarity index 100% rename from assets/images/wiki/ModlistGuides/shared/ProtontricksDiscover.png rename to assets/images/wiki/UserGuide/shared/ProtontricksDiscover.png diff --git a/assets/images/wiki/ModlistGuides/shared/ProtontricksDiscoverInstall.png b/assets/images/wiki/UserGuide/shared/ProtontricksDiscoverInstall.png similarity index 100% rename from assets/images/wiki/ModlistGuides/shared/ProtontricksDiscoverInstall.png rename to assets/images/wiki/UserGuide/shared/ProtontricksDiscoverInstall.png diff --git a/assets/images/wiki/ModlistGuides/shared/Protontricks_GUI-dotfiles.png b/assets/images/wiki/UserGuide/shared/Protontricks_GUI-dotfiles.png similarity index 100% rename from assets/images/wiki/ModlistGuides/shared/Protontricks_GUI-dotfiles.png rename to assets/images/wiki/UserGuide/shared/Protontricks_GUI-dotfiles.png diff --git a/assets/images/wiki/ModlistGuides/shared/ReEnableENBMods.png b/assets/images/wiki/UserGuide/shared/ReEnableENBMods.png similarity index 100% rename from assets/images/wiki/ModlistGuides/shared/ReEnableENBMods.png rename to assets/images/wiki/UserGuide/shared/ReEnableENBMods.png diff --git a/assets/images/wiki/ModlistGuides/shared/SSEDisplayEditINI.png b/assets/images/wiki/UserGuide/shared/SSEDisplayEditINI.png similarity index 100% rename from assets/images/wiki/ModlistGuides/shared/SSEDisplayEditINI.png rename to assets/images/wiki/UserGuide/shared/SSEDisplayEditINI.png diff --git a/assets/images/wiki/ModlistGuides/shared/SSEDisplayEditResolution.png b/assets/images/wiki/UserGuide/shared/SSEDisplayEditResolution.png similarity index 100% rename from assets/images/wiki/ModlistGuides/shared/SSEDisplayEditResolution.png rename to assets/images/wiki/UserGuide/shared/SSEDisplayEditResolution.png diff --git a/assets/images/wiki/ModlistGuides/shared/STEAM_COMPAT_MOUNTS.png b/assets/images/wiki/UserGuide/shared/STEAM_COMPAT_MOUNTS.png similarity index 100% rename from assets/images/wiki/ModlistGuides/shared/STEAM_COMPAT_MOUNTS.png rename to assets/images/wiki/UserGuide/shared/STEAM_COMPAT_MOUNTS.png diff --git a/assets/images/wiki/ModlistGuides/shared/Shared-AddMO2NonSteamGame.png b/assets/images/wiki/UserGuide/shared/Shared-AddMO2NonSteamGame.png similarity index 100% rename from assets/images/wiki/ModlistGuides/shared/Shared-AddMO2NonSteamGame.png rename to assets/images/wiki/UserGuide/shared/Shared-AddMO2NonSteamGame.png diff --git a/assets/images/wiki/ModlistGuides/shared/Shared-AddNonSteamGame.png b/assets/images/wiki/UserGuide/shared/Shared-AddNonSteamGame.png similarity index 100% rename from assets/images/wiki/ModlistGuides/shared/Shared-AddNonSteamGame.png rename to assets/images/wiki/UserGuide/shared/Shared-AddNonSteamGame.png diff --git a/assets/images/wiki/ModlistGuides/shared/Shared-BrowseNonSteamGame.png b/assets/images/wiki/UserGuide/shared/Shared-BrowseNonSteamGame.png similarity index 100% rename from assets/images/wiki/ModlistGuides/shared/Shared-BrowseNonSteamGame.png rename to assets/images/wiki/UserGuide/shared/Shared-BrowseNonSteamGame.png diff --git a/assets/images/wiki/ModlistGuides/shared/Shared-MO2DownloadsError.png b/assets/images/wiki/UserGuide/shared/Shared-MO2DownloadsError.png similarity index 100% rename from assets/images/wiki/ModlistGuides/shared/Shared-MO2DownloadsError.png rename to assets/images/wiki/UserGuide/shared/Shared-MO2DownloadsError.png diff --git a/assets/images/wiki/ModlistGuides/shared/Shared-MO2ExecutablesCog.png b/assets/images/wiki/UserGuide/shared/Shared-MO2ExecutablesCog.png similarity index 100% rename from assets/images/wiki/ModlistGuides/shared/Shared-MO2ExecutablesCog.png rename to assets/images/wiki/UserGuide/shared/Shared-MO2ExecutablesCog.png diff --git a/assets/images/wiki/ModlistGuides/shared/Shared-MO2PortableError.png b/assets/images/wiki/UserGuide/shared/Shared-MO2PortableError.png similarity index 100% rename from assets/images/wiki/ModlistGuides/shared/Shared-MO2PortableError.png rename to assets/images/wiki/UserGuide/shared/Shared-MO2PortableError.png diff --git a/assets/images/wiki/ModlistGuides/shared/Shared-MO2RegisterNXMLinks.png b/assets/images/wiki/UserGuide/shared/Shared-MO2RegisterNXMLinks.png similarity index 100% rename from assets/images/wiki/ModlistGuides/shared/Shared-MO2RegisterNXMLinks.png rename to assets/images/wiki/UserGuide/shared/Shared-MO2RegisterNXMLinks.png diff --git a/assets/images/wiki/ModlistGuides/shared/Shared-ProtonNonSteamGame.png b/assets/images/wiki/UserGuide/shared/Shared-ProtonNonSteamGame.png similarity index 100% rename from assets/images/wiki/ModlistGuides/shared/Shared-ProtonNonSteamGame.png rename to assets/images/wiki/UserGuide/shared/Shared-ProtonNonSteamGame.png diff --git a/assets/images/wiki/ModlistGuides/shared/Shared-ProtonUpQT-AddVersion.png b/assets/images/wiki/UserGuide/shared/Shared-ProtonUpQT-AddVersion.png similarity index 100% rename from assets/images/wiki/ModlistGuides/shared/Shared-ProtonUpQT-AddVersion.png rename to assets/images/wiki/UserGuide/shared/Shared-ProtonUpQT-AddVersion.png diff --git a/assets/images/wiki/ModlistGuides/shared/Shared-ProtonUpQT-InstallVersion.png b/assets/images/wiki/UserGuide/shared/Shared-ProtonUpQT-InstallVersion.png similarity index 100% rename from assets/images/wiki/ModlistGuides/shared/Shared-ProtonUpQT-InstallVersion.png rename to assets/images/wiki/UserGuide/shared/Shared-ProtonUpQT-InstallVersion.png diff --git a/assets/images/wiki/ModlistGuides/shared/Shared-ProtonUpQTDiscoverSearch.png b/assets/images/wiki/UserGuide/shared/Shared-ProtonUpQTDiscoverSearch.png similarity index 100% rename from assets/images/wiki/ModlistGuides/shared/Shared-ProtonUpQTDiscoverSearch.png rename to assets/images/wiki/UserGuide/shared/Shared-ProtonUpQTDiscoverSearch.png diff --git a/assets/images/wiki/ModlistGuides/shared/Shared-ProtontricksDiscover.png b/assets/images/wiki/UserGuide/shared/Shared-ProtontricksDiscover.png similarity index 100% rename from assets/images/wiki/ModlistGuides/shared/Shared-ProtontricksDiscover.png rename to assets/images/wiki/UserGuide/shared/Shared-ProtontricksDiscover.png diff --git a/assets/images/wiki/ModlistGuides/shared/Shared-ProtontricksDiscoverInstall.png b/assets/images/wiki/UserGuide/shared/Shared-ProtontricksDiscoverInstall.png similarity index 100% rename from assets/images/wiki/ModlistGuides/shared/Shared-ProtontricksDiscoverInstall.png rename to assets/images/wiki/UserGuide/shared/Shared-ProtontricksDiscoverInstall.png diff --git a/assets/images/wiki/ModlistGuides/shared/Shared-STEAM_COMPAT_MOUNTS.png b/assets/images/wiki/UserGuide/shared/Shared-STEAM_COMPAT_MOUNTS.png similarity index 100% rename from assets/images/wiki/ModlistGuides/shared/Shared-STEAM_COMPAT_MOUNTS.png rename to assets/images/wiki/UserGuide/shared/Shared-STEAM_COMPAT_MOUNTS.png diff --git a/assets/images/wiki/ModlistGuides/shared/Shared-VCRedistInstallComplete.png b/assets/images/wiki/UserGuide/shared/Shared-VCRedistInstallComplete.png similarity index 100% rename from assets/images/wiki/ModlistGuides/shared/Shared-VCRedistInstallComplete.png rename to assets/images/wiki/UserGuide/shared/Shared-VCRedistInstallComplete.png diff --git a/assets/images/wiki/ModlistGuides/shared/Shared-VCRedistInstallStart.png b/assets/images/wiki/UserGuide/shared/Shared-VCRedistInstallStart.png similarity index 100% rename from assets/images/wiki/ModlistGuides/shared/Shared-VCRedistInstallStart.png rename to assets/images/wiki/UserGuide/shared/Shared-VCRedistInstallStart.png diff --git a/assets/images/wiki/ModlistGuides/shared/Shared-WineShell.png b/assets/images/wiki/UserGuide/shared/Shared-WineShell.png similarity index 100% rename from assets/images/wiki/ModlistGuides/shared/Shared-WineShell.png rename to assets/images/wiki/UserGuide/shared/Shared-WineShell.png diff --git a/assets/images/wiki/ModlistGuides/shared/Shared/Jackify_Github_Banner.png b/assets/images/wiki/UserGuide/shared/Shared/Jackify_Github_Banner.png similarity index 100% rename from assets/images/wiki/ModlistGuides/shared/Shared/Jackify_Github_Banner.png rename to assets/images/wiki/UserGuide/shared/Shared/Jackify_Github_Banner.png diff --git a/assets/images/wiki/ModlistGuides/shared/Shared/mo2-run-button.png b/assets/images/wiki/UserGuide/shared/Shared/mo2-run-button.png similarity index 100% rename from assets/images/wiki/ModlistGuides/shared/Shared/mo2-run-button.png rename to assets/images/wiki/UserGuide/shared/Shared/mo2-run-button.png diff --git a/assets/images/wiki/ModlistGuides/shared/Shared/nexus-jackify-download-page.png b/assets/images/wiki/UserGuide/shared/Shared/nexus-jackify-download-page.png similarity index 100% rename from assets/images/wiki/ModlistGuides/shared/Shared/nexus-jackify-download-page.png rename to assets/images/wiki/UserGuide/shared/Shared/nexus-jackify-download-page.png diff --git a/assets/images/wiki/ModlistGuides/shared/Shared/steam-library-tuxborn-premium.png b/assets/images/wiki/UserGuide/shared/Shared/steam-library-tuxborn-premium.png similarity index 100% rename from assets/images/wiki/ModlistGuides/shared/Shared/steam-library-tuxborn-premium.png rename to assets/images/wiki/UserGuide/shared/Shared/steam-library-tuxborn-premium.png diff --git a/assets/images/wiki/ModlistGuides/shared/Shared_ProtonUp-AddVersion.png b/assets/images/wiki/UserGuide/shared/Shared_ProtonUp-AddVersion.png similarity index 100% rename from assets/images/wiki/ModlistGuides/shared/Shared_ProtonUp-AddVersion.png rename to assets/images/wiki/UserGuide/shared/Shared_ProtonUp-AddVersion.png diff --git a/assets/images/wiki/ModlistGuides/shared/Shared_ProtonUp-QTSearch.png b/assets/images/wiki/UserGuide/shared/Shared_ProtonUp-QTSearch.png similarity index 100% rename from assets/images/wiki/ModlistGuides/shared/Shared_ProtonUp-QTSearch.png rename to assets/images/wiki/UserGuide/shared/Shared_ProtonUp-QTSearch.png diff --git a/assets/images/wiki/ModlistGuides/shared/Shared_ProtonUp-QTsteamtinkerlaunch.png b/assets/images/wiki/UserGuide/shared/Shared_ProtonUp-QTsteamtinkerlaunch.png similarity index 100% rename from assets/images/wiki/ModlistGuides/shared/Shared_ProtonUp-QTsteamtinkerlaunch.png rename to assets/images/wiki/UserGuide/shared/Shared_ProtonUp-QTsteamtinkerlaunch.png diff --git a/assets/images/wiki/ModlistGuides/shared/Shared_STL-CustomCommand.png b/assets/images/wiki/UserGuide/shared/Shared_STL-CustomCommand.png similarity index 100% rename from assets/images/wiki/ModlistGuides/shared/Shared_STL-CustomCommand.png rename to assets/images/wiki/UserGuide/shared/Shared_STL-CustomCommand.png diff --git a/assets/images/wiki/ModlistGuides/shared/VCRedistInstallComplete.png b/assets/images/wiki/UserGuide/shared/VCRedistInstallComplete.png similarity index 100% rename from assets/images/wiki/ModlistGuides/shared/VCRedistInstallComplete.png rename to assets/images/wiki/UserGuide/shared/VCRedistInstallComplete.png diff --git a/assets/images/wiki/ModlistGuides/shared/VCRedistInstallStart.png b/assets/images/wiki/UserGuide/shared/VCRedistInstallStart.png similarity index 100% rename from assets/images/wiki/ModlistGuides/shared/VCRedistInstallStart.png rename to assets/images/wiki/UserGuide/shared/VCRedistInstallStart.png diff --git a/assets/images/wiki/ModlistGuides/shared/WineShell.png b/assets/images/wiki/UserGuide/shared/WineShell.png similarity index 100% rename from assets/images/wiki/ModlistGuides/shared/WineShell.png rename to assets/images/wiki/UserGuide/shared/WineShell.png diff --git a/assets/images/wiki/ModlistGuides/shared/mo2-run-button.png b/assets/images/wiki/UserGuide/shared/mo2-run-button.png similarity index 100% rename from assets/images/wiki/ModlistGuides/shared/mo2-run-button.png rename to assets/images/wiki/UserGuide/shared/mo2-run-button.png diff --git a/assets/images/wiki/ModlistGuides/shared/nexus-jackify-download-page.png b/assets/images/wiki/UserGuide/shared/nexus-jackify-download-page.png similarity index 100% rename from assets/images/wiki/ModlistGuides/shared/nexus-jackify-download-page.png rename to assets/images/wiki/UserGuide/shared/nexus-jackify-download-page.png diff --git a/assets/images/wiki/ModlistGuides/shared/start.png b/assets/images/wiki/UserGuide/shared/start.png similarity index 100% rename from assets/images/wiki/ModlistGuides/shared/start.png rename to assets/images/wiki/UserGuide/shared/start.png diff --git a/assets/images/wiki/ModlistGuides/shared/steam-library-tuxborn-premium.png b/assets/images/wiki/UserGuide/shared/steam-library-tuxborn-premium.png similarity index 100% rename from assets/images/wiki/ModlistGuides/shared/steam-library-tuxborn-premium.png rename to assets/images/wiki/UserGuide/shared/steam-library-tuxborn-premium.png diff --git a/jackify/__init__.py b/jackify/__init__.py index 9bc7204..523edbc 100644 --- a/jackify/__init__.py +++ b/jackify/__init__.py @@ -5,4 +5,4 @@ This package provides both CLI and GUI interfaces for managing Wabbajack modlists natively on Linux systems. """ -__version__ = "0.4.0" +__version__ = "0.5.0" diff --git a/jackify/backend/core/modlist_operations.py b/jackify/backend/core/modlist_operations.py index 1d6b350..e6de2c0 100644 --- a/jackify/backend/core/modlist_operations.py +++ b/jackify/backend/core/modlist_operations.py @@ -107,7 +107,7 @@ def get_jackify_engine_path(): logger.warning(f"AppImage engine not found at expected path: {engine_path}") # Priority 3: Check if THIS process is actually running from Jackify AppImage - # (not just inheriting APPDIR from another AppImage like Cursor) + # (not just inheriting APPDIR from another AppImage context) appdir = os.environ.get('APPDIR') if appdir and sys.argv[0] and 'jackify' in sys.argv[0].lower() and '/tmp/.mount_' in sys.argv[0]: # Only use AppImage path if we're actually running a Jackify AppImage @@ -179,6 +179,92 @@ class ModlistInstallCLI( # Initialize process tracking for cleanup self._current_process = None + @staticmethod + def _normalize_version_token(value: str | None) -> str | None: + if value is None: + return None + token = str(value).strip() + if not token: + return None + return token.lstrip("vV").lower() + + @staticmethod + def _normalize_modlist_name(value: str | None) -> str: + return " ".join((value or "").strip().lower().split()) + + def _get_requested_modlist_version(self) -> str | None: + info = self.context.get("selected_modlist_info") or {} + return self._normalize_version_token(info.get("version")) + + def _evaluate_update_candidate( + self, + modlist_name: str, + install_dir: str, + existing_appid: str | None, + ) -> tuple[bool, dict]: + from jackify.backend.utils.modlist_meta import read_modlist_meta + + result = { + "eligible": False, + "reason": "unknown", + "requested_version": None, + "installed_version": None, + "version_relation": "unknown", + "installed_name": None, + } + if not existing_appid: + result["reason"] = "missing_shortcut_appid" + return False, result + + meta = read_modlist_meta(install_dir) + if not meta: + result["reason"] = "missing_meta" + return False, result + + installed_name = (meta.get("modlist_name") or "").strip() + result["installed_name"] = installed_name + if self._normalize_modlist_name(installed_name) != self._normalize_modlist_name(modlist_name): + result["reason"] = "modlist_name_mismatch" + return False, result + + requested_version = self._get_requested_modlist_version() + installed_version = self._normalize_version_token(meta.get("modlist_version")) + result["requested_version"] = requested_version + result["installed_version"] = installed_version + if requested_version and installed_version: + result["version_relation"] = "same" if requested_version == installed_version else "different" + + result["eligible"] = True + result["reason"] = "eligible" + return True, result + + def _find_existing_shortcut_appid(self, modlist_name: str, install_dir: str) -> str | None: + try: + install_real = os.path.realpath(install_dir) + candidate_exes = [ + os.path.join(install_real, "ModOrganizer.exe"), + os.path.join(install_real, "files", "ModOrganizer.exe"), + ] + + for exe_path in candidate_exes: + if not os.path.exists(exe_path): + continue + appid = self.shortcut_handler.get_appid_from_vdf(modlist_name, exe_path) + if appid: + return appid + + for shortcut in self.shortcut_handler.find_shortcuts_by_exe("ModOrganizer.exe"): + if ( + shortcut.get("AppName", "").strip() == modlist_name.strip() + and os.path.realpath(shortcut.get("StartDir", "")) == install_real + ): + raw_appid = shortcut.get("appid") + if raw_appid is not None: + return str(int(raw_appid) & 0xFFFFFFFF) + except Exception as e: + self.logger.warning("CLI update detection: failed shortcut lookup: %s", e) + return None + def cleanup(self): """Clean up any running jackify-engine process""" if self._current_process and self._current_process.poll() is None: diff --git a/jackify/backend/core/modlist_operations_configuration_cli.py b/jackify/backend/core/modlist_operations_configuration_cli.py index dc845de..204e989 100644 --- a/jackify/backend/core/modlist_operations_configuration_cli.py +++ b/jackify/backend/core/modlist_operations_configuration_cli.py @@ -1,4 +1,5 @@ """CLI configuration phase methods for ModlistInstallCLI (Mixin).""" +import json import logging import os import subprocess @@ -166,19 +167,81 @@ class ModlistOperationsConfigurationCLIMixin: from jackify.backend.handlers.subprocess_utils import get_clean_subprocess_env clean_env = get_clean_subprocess_env() - self._current_process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=False, env=clean_env, cwd=engine_dir) + self._current_process = subprocess.Popen( + cmd, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=False, + env=clean_env, + cwd=engine_dir, + ) proc = self._current_process + def _write_stdin(payload: str) -> bool: + if not proc.stdin or proc.poll() is not None: + return False + try: + proc.stdin.write((payload + '\n').encode('utf-8')) + proc.stdin.flush() + return True + except Exception: + self.logger.debug("Failed writing to engine stdin", exc_info=True) + return False + buffer = b'' inline_progress_active = False + pending_manual = [] while True: chunk = proc.stdout.read(1) if not chunk: break buffer += chunk - if chunk == b'\n': + if chunk in (b'\n', b'\r'): line = buffer.decode('utf-8', errors='replace') + decoded = line.rstrip('\r\n') + if decoded.startswith('{'): + try: + event = json.loads(decoded) + except (json.JSONDecodeError, ValueError): + event = None + if event: + event_name = event.get('event') + if event_name == 'manual_download_required': + pending_manual.append(event) + buffer = b'' + continue + if event_name == 'manual_download_list_complete': + loop_iter = event.get('loop_iteration', 1) + for item in pending_manual: + item['loop_iteration'] = loop_iter + from jackify.backend.handlers.config_handler import ConfigHandler + raw_limit = ConfigHandler().get('manual_download_concurrent_limit', 2) + try: + manual_limit = int(raw_limit) + except (TypeError, ValueError): + manual_limit = 2 + from jackify.frontends.cli.commands.manual_download_flow import run_cli_manual_download_phase + completed = run_cli_manual_download_phase( + events=list(pending_manual), + loop_iteration=loop_iter, + download_dir=actual_download_path, + stdin_write=_write_stdin, + concurrent_limit=max(1, min(5, manual_limit)), + ) + if not completed: + if proc.poll() is None: + proc.terminate() + buffer = b'' + break + pending_manual.clear() + buffer = b'' + continue + if event_name == 'manual_download_phase_complete': + print("All manual downloads confirmed. Resuming installation...") + buffer = b'' + continue if '[FILE_PROGRESS]' in line: parts = line.split('[FILE_PROGRESS]', 1) if parts[0].strip(): @@ -197,26 +260,6 @@ class ModlistOperationsConfigurationCLIMixin: inline_progress_active = False print(line, end='') buffer = b'' - elif chunk == b'\r': - line = buffer.decode('utf-8', errors='replace') - if '[FILE_PROGRESS]' in line: - parts = line.split('[FILE_PROGRESS]', 1) - if parts[0].strip(): - line = parts[0].rstrip() - else: - buffer = b'' - continue - clean_line = line.rstrip('\r\n') - if clean_line.startswith("Installing files "): - print(f"\r{clean_line}", end='') - inline_progress_active = True - else: - if inline_progress_active: - print() - inline_progress_active = False - print(line, end='') - sys.stdout.flush() - buffer = b'' if buffer: line = buffer.decode('utf-8', errors='replace') @@ -400,6 +443,16 @@ class ModlistOperationsConfigurationCLIMixin: app_id = None use_automated_prefix = os.environ.get('JACKIFY_USE_AUTOMATED_PREFIX', '1') == '1' + existing_shortcut_appid = self.context.get('existing_shortcut_appid') + update_existing_install = bool(self.context.get('update_existing_install')) + + if update_existing_install and existing_shortcut_appid: + app_id = str(existing_shortcut_appid) + success = True + prefix_path = None + result = True + print(f"\n{COLOR_INFO}Update mode selected. Reusing existing Steam shortcut AppID {app_id}.{COLOR_RESET}") + use_automated_prefix = False if use_automated_prefix: print(f"\n{COLOR_INFO}Using automated Steam setup workflow...{COLOR_RESET}") @@ -535,17 +588,20 @@ class ModlistOperationsConfigurationCLIMixin: success, prefix_path, app_id = True, None, None else: success, prefix_path, app_id = False, None, None - - if success: + if success: + if update_existing_install and app_id: + print(f"{COLOR_SUCCESS}Update mode Steam setup confirmed.{COLOR_RESET}") + print(f"{COLOR_INFO}Reusing Steam AppID: {app_id}{COLOR_RESET}") + else: print(f"{COLOR_SUCCESS}Automated Steam setup completed successfully!{COLOR_RESET}") if prefix_path: print(f"{COLOR_INFO}Proton prefix created at: {prefix_path}{COLOR_RESET}") if app_id: print(f"{COLOR_INFO}Steam AppID: {app_id}{COLOR_RESET}") - else: - print(f"{COLOR_ERROR}Automated Steam setup failed. Result: {result}{COLOR_RESET}") - print(f"{COLOR_ERROR}Steam integration was not completed. Please check the logs for details.{COLOR_RESET}") - return + else: + print(f"{COLOR_ERROR}Automated Steam setup failed. Result: {result}{COLOR_RESET}") + print(f"{COLOR_ERROR}Steam integration was not completed. Please check the logs for details.{COLOR_RESET}") + return from jackify.backend.services.modlist_service import ModlistService from jackify.backend.models.modlist import ModlistContext @@ -572,18 +628,28 @@ class ModlistOperationsConfigurationCLIMixin: progress_callback("") progress_callback("=== Configuration Phase ===") - print(f"\n{COLOR_INFO}=== Configuration Phase ==={COLOR_RESET}") - self.logger.info("Running post-installation configuration phase using ModlistService") + print(f"\n{COLOR_INFO}=== Configuration Phase ==={COLOR_RESET}") + self.logger.info("Running post-installation configuration phase using ModlistService") configuration_success = modlist_service.configure_modlist_post_steam(modlist_context) if configuration_success: - print(f"{COLOR_SUCCESS}Configuration completed successfully!{COLOR_RESET}") self.logger.info("Post-installation configuration completed successfully") + print(f"{COLOR_INFO}Core configuration complete. Checking post-install automation...{COLOR_RESET}") try: # Ensure CLI install flow gets the same VNV automation behavior as GUI. - from jackify.backend.services.vnv_integration_helper import run_vnv_automation_if_applicable + 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: @@ -593,31 +659,47 @@ class ModlistOperationsConfigurationCLIMixin: except (EOFError, KeyboardInterrupt): return False return user_input in ("", "y", "yes") - def _manual_vnv_file(title: str, instructions: str): - print(f"\n{COLOR_WARNING}{title}{COLOR_RESET}") - print(instructions) - try: - file_input = input(f"{COLOR_PROMPT}Path to downloaded file: {COLOR_RESET}").strip() - except (EOFError, KeyboardInterrupt): - return None - if not file_input: - return None - selected = Path(file_input).expanduser().resolve() - return selected if selected.exists() else None - automation_ran, vnv_error = run_vnv_automation_if_applicable( - modlist_name=modlist_name_for_automation, - modlist_install_location=Path(install_dir_str), - game_root=None, # Auto-detect from modlist structure. - ttw_installer_path=AutomatedPrefixService.get_ttw_installer_path(), - progress_callback=lambda msg: print(msg), - manual_file_callback=_manual_vnv_file, - confirmation_callback=_confirm_vnv, - ) - 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}") + 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}") @@ -632,6 +714,7 @@ class ModlistOperationsConfigurationCLIMixin: 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") diff --git a/jackify/backend/core/modlist_operations_configuration_gui.py b/jackify/backend/core/modlist_operations_configuration_gui.py index dd2fb9b..1e45add 100644 --- a/jackify/backend/core/modlist_operations_configuration_gui.py +++ b/jackify/backend/core/modlist_operations_configuration_gui.py @@ -68,7 +68,7 @@ class ModlistOperationsConfigurationGUIMixin: if result: if completion_callback: - completion_callback(True, "Configuration completed successfully!", config_context['name']) + completion_callback(True, "Core configuration complete", config_context['name']) return True else: retry_count += 1 @@ -139,7 +139,7 @@ class ModlistOperationsConfigurationGUIMixin: if result: if completion_callback: - completion_callback(True, "Configuration completed successfully!", config_context['name']) + completion_callback(True, "Core configuration complete", config_context['name']) return True else: if progress_callback: diff --git a/jackify/backend/core/modlist_operations_discovery.py b/jackify/backend/core/modlist_operations_discovery.py index 6a11786..6e9a24b 100644 --- a/jackify/backend/core/modlist_operations_discovery.py +++ b/jackify/backend/core/modlist_operations_discovery.py @@ -243,6 +243,46 @@ class ModlistOperationsDiscoveryMixin: self.context['download_dir'] = download_dir_path self.logger.debug(f"Download directory context set to: {self.context['download_dir']}") + install_dir_value = self.context.get('install_dir') + install_dir_real = os.path.realpath(str(install_dir_value[0] if isinstance(install_dir_value, tuple) else install_dir_value)) + existing_appid = self._find_existing_shortcut_appid(self.context['modlist_name'], install_dir_real) + eligible_update, update_meta = self._evaluate_update_candidate( + self.context['modlist_name'], + install_dir_real, + existing_appid, + ) + if eligible_update: + print("\n" + "-" * 28) + print(f"{COLOR_WARNING}Existing modlist installation detected in this directory.{COLOR_RESET}") + relation = update_meta.get("version_relation") + if relation == "different": + print( + f"{COLOR_INFO}Detected version change: installed v{update_meta.get('installed_version')} -> " + f"selected v{update_meta.get('requested_version')}.{COLOR_RESET}" + ) + elif relation == "same" and update_meta.get("installed_version"): + print( + f"{COLOR_INFO}Detected same version (v{update_meta.get('installed_version')}). " + "Use update mode for repair/reconfigure behavior." + f"{COLOR_RESET}" + ) + print("Choose how to proceed:") + print(" 1. Update existing install (recommended)") + print(" 2. New install with a different Steam shortcut name") + print(" 0. Cancel") + update_choice = input(f"{COLOR_PROMPT}Enter your selection (0-2): {COLOR_RESET}").strip() + if update_choice == "1": + self.context['update_existing_install'] = True + self.context['existing_shortcut_appid'] = existing_appid + self.logger.info("CLI update mode selected; reusing AppID %s", existing_appid) + elif update_choice == "2": + print( + f"{COLOR_WARNING}For a new install, choose a different Modlist Name before proceeding.{COLOR_RESET}" + ) + return None + else: + self.logger.info("User cancelled at CLI update detection prompt.") + return None + if 'nexus_api_key' not in self.context or not self.context.get('nexus_api_key'): from jackify.backend.services.nexus_auth_service import NexusAuthService auth_service = NexusAuthService() diff --git a/jackify/backend/handlers/config_handler.py b/jackify/backend/handlers/config_handler.py index 6968625..a458fe7 100644 --- a/jackify/backend/handlers/config_handler.py +++ b/jackify/backend/handlers/config_handler.py @@ -17,6 +17,10 @@ from typing import Optional from .config_handler_encryption import ConfigEncryptionMixin from .config_handler_directories import ConfigDirectoriesMixin from .config_handler_proton import ConfigProtonMixin +from jackify.shared.steam_utils import ( + STEAM_PREFERENCE_AUTO, + resolve_preferred_steam_installation, +) logger = logging.getLogger(__name__) @@ -50,6 +54,7 @@ class ConfigHandler(ConfigEncryptionMixin, ConfigDirectoriesMixin, ConfigProtonM "resolution": None, "protontricks_path": None, "steam_path": None, + "steam_install_preference": STEAM_PREFERENCE_AUTO, # auto|flatpak|native "nexus_api_key": None, # Base64 encoded API key "default_install_parent_dir": None, # Parent directory for modlist installations "default_download_parent_dir": None, # Parent directory for downloads @@ -62,6 +67,8 @@ class ConfigHandler(ConfigEncryptionMixin, ConfigDirectoriesMixin, ConfigProtonM "proton_path": None, # Install Proton path (for jackify-engine) - None means auto-detect "proton_version": None, # Install Proton version name - None means auto-detect "steam_restart_strategy": "jackify", # "jackify" (default) or "simple" + "manual_download_concurrent_limit": 2, # Shared GUI/CLI default for manual download browser tabs + "manual_download_watch_directory": None, # Optional override for manual-download watcher folder "window_width": None, # Saved window width (None = use dynamic sizing) "window_height": None # Saved window height (None = use dynamic sizing) } @@ -72,14 +79,13 @@ class ConfigHandler(ConfigEncryptionMixin, ConfigDirectoriesMixin, ConfigProtonM # Perform version migrations self._migrate_config() + # Normalize/repair Proton selections on every startup so stale deleted versions + # cannot break workflows. + self.normalize_proton_paths_on_boot() + # If steam_path is not set, detect it if not self.settings["steam_path"]: self.settings["steam_path"] = self._detect_steam_path() - - # Auto-detect and set Proton version ONLY on first run (config file doesn't exist) - # Do NOT overwrite user's saved settings! - if not os.path.exists(self.config_file) and not self.settings.get("proton_path"): - self._auto_detect_proton() # If jackify_data_dir is not set, initialize it to default if not self.settings.get("jackify_data_dir"): @@ -95,35 +101,16 @@ class ConfigHandler(ConfigEncryptionMixin, ConfigDirectoriesMixin, ConfigProtonM str: Path to the Steam installation or None if not found """ logger.info("Detecting Steam installation path...") - - # Common Steam installation paths - steam_paths = [ - os.path.expanduser("~/.steam/steam"), - os.path.expanduser("~/.local/share/Steam"), - os.path.expanduser("~/.steam/root") - ] - - # Check each path - for path in steam_paths: - if os.path.exists(path): - logger.info(f"Found Steam installation at: {path}") - return path - - # If not found in common locations, try to find using libraryfolders.vdf - libraryfolders_vdf_paths = [ - os.path.expanduser("~/.steam/steam/config/libraryfolders.vdf"), - os.path.expanduser("~/.local/share/Steam/config/libraryfolders.vdf"), - os.path.expanduser("~/.steam/root/config/libraryfolders.vdf"), - os.path.expanduser("~/.var/app/com.valvesoftware.Steam/.local/share/Steam/config/libraryfolders.vdf") # Flatpak - ] - - for vdf_path in libraryfolders_vdf_paths: - if os.path.exists(vdf_path): - # Extract the Steam path from the libraryfolders.vdf path - steam_path = os.path.dirname(os.path.dirname(vdf_path)) - logger.info(f"Found Steam installation at: {steam_path}") - return steam_path - + preference = self.settings.get("steam_install_preference", STEAM_PREFERENCE_AUTO) + install_type, install_root = resolve_preferred_steam_installation(preference=preference) + if install_root: + logger.info( + "Selected Steam installation: %s (%s)", + install_type, + install_root, + ) + return str(install_root) + logger.error("Steam installation not found") return None @@ -376,4 +363,4 @@ class ConfigHandler(ConfigEncryptionMixin, ConfigDirectoriesMixin, ConfigProtonM - \ No newline at end of file + diff --git a/jackify/backend/handlers/config_handler_proton.py b/jackify/backend/handlers/config_handler_proton.py index 872ce38..2e2b1ce 100644 --- a/jackify/backend/handlers/config_handler_proton.py +++ b/jackify/backend/handlers/config_handler_proton.py @@ -3,6 +3,8 @@ Config handler Proton path and version getters and auto-detect. """ import logging +from pathlib import Path +from typing import Optional, Dict, Any logger = logging.getLogger(__name__) @@ -10,6 +12,105 @@ logger = logging.getLogger(__name__) class ConfigProtonMixin: """Mixin providing Proton path/version and auto-detect for ConfigHandler.""" + @staticmethod + def _is_usable_proton_path(proton_path: Optional[str]) -> bool: + """Return True when path looks like a valid Proton install directory.""" + if not proton_path: + return False + try: + p = Path(str(proton_path)).expanduser() + if not p.is_dir(): + return False + # Valve Proton structure + if (p / "dist" / "bin" / "wine").exists(): + return True + # GE-Proton structure + if (p / "files" / "bin" / "wine").exists(): + return True + return False + except Exception: + return False + + @staticmethod + def _best_proton_entry() -> Optional[Dict[str, Any]]: + """Get best detected Proton entry or None.""" + try: + from .wine_utils import WineUtils + return WineUtils.select_best_proton() + except Exception: + return None + + def normalize_proton_paths_on_boot(self) -> bool: + """ + Ensure stored Proton paths are valid at startup, repairing stale selections. + + Rules: + - If install proton path is missing/invalid, auto-detect next best and persist it. + - If no compatible Proton exists, persist install path/version as null. + - If game proton path is set and invalid, reset it to install proton (or null). + + Returns: + True if config values were changed and saved, False otherwise. + """ + changed = False + + install_path = self.settings.get("proton_path") + if install_path == "auto": + install_path = None + + install_valid = self._is_usable_proton_path(install_path) + if not install_valid: + best = self._best_proton_entry() + if best: + best_path = str(best["path"]) + best_name = str(best.get("name") or Path(best_path).name) + if self.settings.get("proton_path") != best_path: + self.settings["proton_path"] = best_path + changed = True + if self.settings.get("proton_version") != best_name: + self.settings["proton_version"] = best_name + changed = True + logger.warning( + "Install Proton path was missing/invalid; auto-selected %s (%s)", + best_name, + best_path, + ) + else: + if self.settings.get("proton_path") is not None: + self.settings["proton_path"] = None + changed = True + if self.settings.get("proton_version") is not None: + self.settings["proton_version"] = None + changed = True + logger.warning( + "Install Proton path was missing/invalid and no compatible Proton was found" + ) + else: + # Keep proton_version in sync with existing valid path when missing/legacy. + if not self.settings.get("proton_version"): + self.settings["proton_version"] = Path(str(install_path)).name + changed = True + + effective_install = self.settings.get("proton_path") + game_path = self.settings.get("game_proton_path") + + # Legacy/placeholder values should not persist for runtime resolution. + if game_path in ("same_as_install", "auto"): + target = effective_install + if self.settings.get("game_proton_path") != target: + self.settings["game_proton_path"] = target + changed = True + elif game_path and not self._is_usable_proton_path(game_path): + self.settings["game_proton_path"] = effective_install + changed = True + logger.warning( + "Game Proton path was missing/invalid; reset to install Proton path" + ) + + if changed: + self.save_config() + return changed + def get_proton_path(self): """Retrieve the saved Install Proton path. Always reads fresh from disk.""" try: diff --git a/jackify/backend/handlers/menu_handler_modlist.py b/jackify/backend/handlers/menu_handler_modlist.py index 125c1a9..9465be2 100644 --- a/jackify/backend/handlers/menu_handler_modlist.py +++ b/jackify/backend/handlers/menu_handler_modlist.py @@ -279,46 +279,56 @@ class ModlistMenuHandler: timestamp = f"[{hours:02d}:{minutes:02d}:{seconds:02d}]" print(f"{COLOR_INFO}{timestamp} {message}{COLOR_RESET}") - # Run the automated workflow - result = prefix_service.run_working_workflow( - modlist_name, install_dir, mo2_path, progress_callback, steamdeck=self.steamdeck - ) - - # Handle the result - if isinstance(result, tuple) and len(result) == 4: - if result[0] == "CONFLICT": - # Handle conflict - ask user what to do - 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(" 1. Use existing shortcut (recommended)") - print(" 2. Create new shortcut anyway") - choice = input(f"{COLOR_PROMPT}Enter choice (1-2): {COLOR_RESET}").strip() - if choice == "1": - # Use existing shortcut - existing_appid = conflicts[0].get('appid') - if existing_appid: - context = { - "name": modlist_name, - "appid": str(existing_appid), - "path": mo2_dir, - "manual_steps_completed": True, - "resolution": None - } - return self.run_modlist_configuration_phase(context) - elif choice == "2": - # Create new shortcut - would need to handle this, but for now just fail - print(f"{COLOR_ERROR}Creating new shortcut with same name not supported in this flow.{COLOR_RESET}") - return True - else: + while True: + result = prefix_service.run_working_workflow( + modlist_name, install_dir, mo2_path, progress_callback, steamdeck=self.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(" 1. Use existing shortcut (recommended)") + print(" 2. Choose a different shortcut name") + choice = input(f"{COLOR_PROMPT}Enter choice (1-2): {COLOR_RESET}").strip() + if choice == "1": + existing_appid = conflicts[0].get('appid') + if existing_appid: + context = { + "name": modlist_name, + "appid": str(existing_appid), + "path": mo2_dir, + "manual_steps_completed": True, + "resolution": None + } + return self.run_modlist_configuration_phase(context) + print(f"{COLOR_ERROR}Could not determine existing shortcut AppID.{COLOR_RESET}") + return True + if choice == "2": + print("") + print(f"{COLOR_PROMPT}Enter a different shortcut name for this modlist.{COLOR_RESET}") + print(f"{COLOR_INFO}(Current conflicting name: {modlist_name}){COLOR_RESET}") + new_name = input(f"{COLOR_PROMPT}New shortcut name (or 'q' to cancel): {COLOR_RESET}").strip() + if new_name.lower() == 'q': + print(f"{COLOR_INFO}Configuration cancelled by user.{COLOR_RESET}") + return True + if not new_name: + print(f"{COLOR_ERROR}Name cannot be empty.{COLOR_RESET}") + continue + if new_name == modlist_name: + print(f"{COLOR_ERROR}Please enter a different name to resolve the conflict.{COLOR_RESET}") + continue + modlist_name = new_name + print(f"{COLOR_INFO}Retrying Steam setup with shortcut name: {modlist_name}{COLOR_RESET}") + continue print(f"{COLOR_ERROR}Invalid choice.{COLOR_RESET}") return True - else: - # Success - get the results + success, prefix_path, appid_int, last_timestamp = result if success and appid_int: context = { @@ -330,10 +340,9 @@ class ModlistMenuHandler: } self.logger.debug(f"[DEBUG] New Modlist Context (automated workflow): {context}") return self.run_modlist_configuration_phase(context) - else: - print(f"{COLOR_ERROR}Automated workflow completed but no AppID was returned.{COLOR_RESET}") - return True - else: + print(f"{COLOR_ERROR}Automated workflow completed but no AppID was returned.{COLOR_RESET}") + return True + # Unexpected result format print(f"{COLOR_ERROR}Automated workflow returned unexpected format.{COLOR_RESET}") self.logger.error(f"Unexpected result format from automated workflow: {result}") @@ -566,8 +575,18 @@ class ModlistMenuHandler: # Run modlist-specific post-install automation (e.g., VNV) before showing completion # Only in CLI mode - GUI handles this in install_modlist.py if not gui_mode: - from jackify.backend.services.vnv_integration_helper import run_vnv_automation_if_applicable + 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, + ) from pathlib import Path modlist_name = context.get('name', '') @@ -581,33 +600,46 @@ class ModlistMenuHandler: except (EOFError, KeyboardInterrupt): return False return user_input in ("", "y", "yes") - - def _manual_vnv_file(title: str, instructions: str): - print(f"\n{COLOR_WARNING}{title}{COLOR_RESET}") - print(instructions) - try: - file_input = input(f"{COLOR_PROMPT}Path to downloaded file: {COLOR_RESET}").strip() - except (EOFError, KeyboardInterrupt): - return None - if not file_input: - return None - selected = Path(file_input).expanduser().resolve() - return selected if selected.exists() else None - - automation_ran, error = run_vnv_automation_if_applicable( - modlist_name=modlist_name, - modlist_install_location=modlist_path, - game_root=None, # Will be auto-detected - ttw_installer_path=AutomatedPrefixService.get_ttw_installer_path(), - progress_callback=lambda msg: print(msg), - manual_file_callback=_manual_vnv_file, - confirmation_callback=_confirm_vnv - ) - if automation_ran and not error: - print(f"{COLOR_INFO}VNV post-install automation completed.{COLOR_RESET}") - if error: - print(f"{COLOR_WARNING}VNV automation encountered an error: {error}{COLOR_RESET}") - print(f"{COLOR_INFO}You can complete these steps manually by following: https://vivanewvegas.moddinglinked.com/wabbajack.html{COLOR_RESET}") + if should_offer_vnv_automation(modlist_name, modlist_path): + game_paths = PathHandler().find_vanilla_game_paths() + resolved_game_root = game_paths.get('Fallout New Vegas') + vnv_service = VNVPostInstallService( + modlist_install_location=modlist_path, + game_root=resolved_game_root or modlist_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, error = run_vnv_automation_if_applicable( + modlist_name=modlist_name, + modlist_install_location=modlist_path, + game_root=None, # Will be auto-detected + 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 error: + print(f"{COLOR_INFO}VNV post-install automation completed.{COLOR_RESET}") + if error: + print(f"{COLOR_WARNING}VNV automation encountered an error: {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 e: self.logger.debug(f"VNV automation check skipped: {e}") # Not an error - just means VNV automation wasn't applicable diff --git a/jackify/backend/handlers/modlist_configuration.py b/jackify/backend/handlers/modlist_configuration.py index d7beba7..8377ef7 100644 --- a/jackify/backend/handlers/modlist_configuration.py +++ b/jackify/backend/handlers/modlist_configuration.py @@ -401,6 +401,18 @@ class ModlistConfigurationMixin: else: self.logger.warning("Could not set download_directory in ModOrganizer.ini") + # Step 8.5: Align /home vs /var/home basis for Z: paths to match modlist install directory. + # This is intentionally separate from broad binary-path rewriting so it still runs when + # engine-installed workflows skip edit_binary_working_paths. + if not self.path_handler.align_home_path_basis( + modlist_ini_path=modlist_ini_path_obj, + modlist_dir_path=modlist_dir_path_obj, + modlist_sdcard=self.modlist_sdcard, + ): + self.logger.error("Failed to align home-path basis in ModOrganizer.ini. Configuration aborted.") + self.logger.error("Failed to align /home path basis in ModOrganizer.ini.") + return False + self.logger.info("Step 8: Updating ModOrganizer.ini paths... Done") # Step 9: Update Resolution Settings (if applicable) @@ -539,6 +551,9 @@ class ModlistConfigurationMixin: else: self.logger.debug("Step 13: No special launch options needed for this modlist type") + if status_callback: + status_callback(f"{self._get_progress_timestamp()} Finalizing post-install configuration") + # Do not call status_callback here, the final message is handled in menu_handler # if status_callback: # status_callback("Configuration completed successfully!") @@ -546,6 +561,8 @@ class ModlistConfigurationMixin: self.logger.info("Configuration steps completed successfully.") # Step 14: Re-enforce Windows 10 mode after modlist-specific configurations (matches legacy script line 1333) + if status_callback: + status_callback(f"{self._get_progress_timestamp()} Re-applying final Windows compatibility settings") self._re_enforce_windows_10_mode() return True # Return True on success @@ -581,4 +598,3 @@ class ModlistConfigurationMixin: else: self.selected_resolution = None self.logger.info("Resolution setup skipped by user.") - diff --git a/jackify/backend/handlers/modlist_install_cli_ttw.py b/jackify/backend/handlers/modlist_install_cli_ttw.py index c1a8bf4..cd22bc1 100644 --- a/jackify/backend/handlers/modlist_install_cli_ttw.py +++ b/jackify/backend/handlers/modlist_install_cli_ttw.py @@ -7,6 +7,7 @@ import shutil from pathlib import Path from .ui_colors import COLOR_PROMPT, COLOR_INFO, COLOR_ERROR, COLOR_RESET, COLOR_WARNING +from jackify.shared.paths import get_jackify_logs_dir logger = logging.getLogger(__name__) @@ -352,10 +353,16 @@ class ModlistInstallCLITTWMixin: print(f"\nTTW has been installed to: {ttw_output_path}") print(f"TTW has been integrated into '{modlist_name}' (modlist.txt + plugins.txt updated).") print(f"The modlist '{modlist_name}' is now ready to use with TTW.") + print(f"Detailed log available at: {get_jackify_logs_dir() / 'TTW_Install_workflow.log'}") + input(f"\n{COLOR_PROMPT}Press Enter to continue...{COLOR_RESET}") else: print(f"\n{COLOR_ERROR}TTW installation failed. Check the logs for details.{COLOR_RESET}") print(f"{COLOR_ERROR}Error: {message}{COLOR_RESET}") + print(f"{COLOR_INFO}Detailed log available at: {get_jackify_logs_dir() / 'TTW_Install_workflow.log'}{COLOR_RESET}") + input(f"\n{COLOR_PROMPT}Press Enter to continue...{COLOR_RESET}") except Exception as e: self.logger.error(f"Error during TTW installation: {e}", exc_info=True) print(f"{COLOR_ERROR}Error during TTW installation: {e}{COLOR_RESET}") + print(f"{COLOR_INFO}Detailed log available at: {get_jackify_logs_dir() / 'TTW_Install_workflow.log'}{COLOR_RESET}") + input(f"\n{COLOR_PROMPT}Press Enter to continue...{COLOR_RESET}") diff --git a/jackify/backend/handlers/path_handler_mo2.py b/jackify/backend/handlers/path_handler_mo2.py index b5ccacd..46b62b5 100644 --- a/jackify/backend/handlers/path_handler_mo2.py +++ b/jackify/backend/handlers/path_handler_mo2.py @@ -28,6 +28,95 @@ SDCARD_PREFIX = '/run/media/mmcblk0p1/' class PathHandlerMO2Mixin: """Mixin providing ModOrganizer.ini path updates and formatting.""" + @staticmethod + def _desired_home_basis_from_modlist_dir(modlist_dir_path: Path) -> Optional[str]: + """ + Determine desired Linux home-path basis from modlist install directory. + + Returns: + "/var/home" when modlist dir is under /var/home, + "/home" when modlist dir is under /home, + None otherwise. + """ + try: + posix = modlist_dir_path.as_posix() + except Exception: + posix = str(modlist_dir_path).replace("\\", "/") + if posix.startswith("/var/home/"): + return "/var/home" + if posix.startswith("/home/"): + return "/home" + return None + + @staticmethod + def _rewrite_z_home_basis_in_line(line: str, desired_home_basis: str) -> str: + """ + Rewrite only Z:-drive /home -> /var/home path basis in a single INI line. + + Preserves slash style (forward or backslash), and leaves D: paths untouched. + """ + if desired_home_basis == "/var/home": + # Z:/home/... -> Z:/var/home/... + # Z:\\home\\... -> Z:\\var\\home\\... + return re.sub(r'([Zz]:[/\\]+)home([/\\]+)', r'\1var\2home\2', line) + return line + + def align_home_path_basis(self, modlist_ini_path: Path, modlist_dir_path: Path, modlist_sdcard: bool) -> bool: + """ + Align gamePath/binary/workingDirectory home-path basis to modlist_dir_path. + + This is a targeted post-processing step for Z: paths only: + - If install path is /var/home/... then rewrite Z:/home/... to Z:/var/home/... + - Otherwise do nothing. + """ + if modlist_sdcard: + return True + desired_home_basis = self._desired_home_basis_from_modlist_dir(modlist_dir_path) + # This alignment pass is intentionally one-way: + # only promote Z:/home -> Z:/var/home when install dir uses /var/home. + if desired_home_basis != "/var/home": + return True + if not modlist_ini_path.is_file(): + logger.error(f"INI file {modlist_ini_path} does not exist for home-basis alignment") + return False + try: + with open(modlist_ini_path, 'r', encoding='utf-8') as f: + lines = f.readlines() + + changed = 0 + for i, line in enumerate(lines): + stripped = line.strip() + if not ( + re.match(r'^\s*gamepath\s*=.*$', stripped, re.IGNORECASE) + or re.match(r'^(\d+)(\\+)\s*binary\s*=.*$', stripped, re.IGNORECASE) + or re.match(r'^(\d+)(\\+)\s*workingDirectory\s*=.*$', stripped, re.IGNORECASE) + ): + continue + rewritten = self._rewrite_z_home_basis_in_line(line, desired_home_basis) + if rewritten != line: + lines[i] = rewritten + changed += 1 + + if changed > 0: + with open(modlist_ini_path, 'w', encoding='utf-8') as f: + f.writelines(lines) + logger.info( + "Aligned ModOrganizer.ini home-path basis to %s for %d line(s): %s", + desired_home_basis, + changed, + modlist_ini_path, + ) + else: + logger.debug( + "No home-path basis alignment needed for %s (target %s)", + modlist_ini_path, + desired_home_basis, + ) + return True + except Exception as e: + logger.error(f"Error aligning home path basis in {modlist_ini_path}: {e}") + return False + @staticmethod def _strip_sdcard_path_prefix(path_obj: Path) -> str: """Removes SD card mount prefix. Returns path as POSIX-style string.""" diff --git a/jackify/backend/handlers/path_handler_steam.py b/jackify/backend/handlers/path_handler_steam.py index 7643296..f15bd4a 100644 --- a/jackify/backend/handlers/path_handler_steam.py +++ b/jackify/backend/handlers/path_handler_steam.py @@ -12,6 +12,10 @@ from pathlib import Path from typing import Optional, List from datetime import datetime import vdf +from jackify.shared.steam_utils import ( + get_ordered_steam_roots, + STEAM_PREFERENCE_AUTO, +) logger = logging.getLogger(__name__) @@ -23,11 +27,7 @@ class PathHandlerSteamMixin: def find_steam_config_vdf() -> Optional[Path]: """Finds the active Steam config.vdf file.""" logger.debug("Searching for Steam config.vdf...") - possible_steam_paths = [ - Path.home() / ".steam/steam", - Path.home() / ".local/share/Steam", - Path.home() / ".steam/root" - ] + possible_steam_paths = get_ordered_steam_roots(STEAM_PREFERENCE_AUTO) for steam_path in possible_steam_paths: potential_path = steam_path / "config/config.vdf" if potential_path.is_file(): @@ -40,10 +40,9 @@ class PathHandlerSteamMixin: def find_steam_library() -> Optional[Path]: """Find the primary Steam library common directory containing games.""" logger.debug("Attempting to find Steam library...") + ordered_roots = get_ordered_steam_roots(STEAM_PREFERENCE_AUTO) libraryfolders_vdf_paths = [ - os.path.expanduser("~/.steam/steam/config/libraryfolders.vdf"), - os.path.expanduser("~/.local/share/Steam/config/libraryfolders.vdf"), - os.path.expanduser("~/.var/app/com.valvesoftware.Steam/.local/share/Steam/config/libraryfolders.vdf"), + str(root / "config" / "libraryfolders.vdf") for root in ordered_roots ] for path in libraryfolders_vdf_paths: if os.path.exists(path): @@ -92,14 +91,11 @@ class PathHandlerSteamMixin: logger.info(f"Using Steam library common path: {library_paths[0]}") return library_paths[0] logger.debug("No valid common paths found in VDF, checking default location...") - default_common_path = Path.home() / ".steam/steam/steamapps/common" - if default_common_path.is_dir(): - logger.info(f"Using default Steam library common path: {default_common_path}") - return default_common_path - default_common_path_local = Path.home() / ".local/share/Steam/steamapps/common" - if default_common_path_local.is_dir(): - logger.info(f"Using default local Steam library common path: {default_common_path_local}") - return default_common_path_local + for root in ordered_roots: + default_common_path = root / "steamapps" / "common" + if default_common_path.is_dir(): + logger.info(f"Using default Steam library common path: {default_common_path}") + return default_common_path logger.error("No valid Steam library common path found in VDF or default locations.") return None except Exception as e: @@ -181,12 +177,8 @@ class PathHandlerSteamMixin: def get_all_steam_library_paths() -> List[Path]: """Finds all Steam library paths listed in all known libraryfolders.vdf files (including Flatpak).""" logger.info("[DEBUG] Searching for all Steam libraryfolders.vdf files...") - vdf_paths = [ - Path.home() / ".steam/steam/config/libraryfolders.vdf", - Path.home() / ".local/share/Steam/config/libraryfolders.vdf", - Path.home() / ".steam/root/config/libraryfolders.vdf", - Path.home() / ".var/app/com.valvesoftware.Steam/.local/share/Steam/config/libraryfolders.vdf", - ] + ordered_roots = get_ordered_steam_roots(STEAM_PREFERENCE_AUTO) + vdf_paths = [root / "config" / "libraryfolders.vdf" for root in ordered_roots] library_paths = set() for vdf_path in vdf_paths: if vdf_path.is_file(): diff --git a/jackify/backend/handlers/subprocess_utils.py b/jackify/backend/handlers/subprocess_utils.py index db23d2b..3db315b 100644 --- a/jackify/backend/handlers/subprocess_utils.py +++ b/jackify/backend/handlers/subprocess_utils.py @@ -6,6 +6,7 @@ import resource import sys import shutil import logging +import threading def get_safe_python_executable(): """ @@ -154,7 +155,7 @@ class ProcessManager: """ Shared process manager for robust subprocess launching, tracking, and cancellation. """ - def __init__(self, cmd, env=None, cwd=None, text=False, bufsize=0, separate_stderr=False): + def __init__(self, cmd, env=None, cwd=None, text=False, bufsize=0, separate_stderr=False, enable_stdin=False): self.cmd = cmd # Default to cleaned environment if None to prevent AppImage variable inheritance if env is None: @@ -165,14 +166,18 @@ class ProcessManager: self.text = text self.bufsize = bufsize self.separate_stderr = separate_stderr + self.enable_stdin = enable_stdin self.proc = None self.process_group_pid = None + self._stdin_lock = threading.Lock() self._start_process() def _start_process(self): stderr_arg = subprocess.PIPE if self.separate_stderr else subprocess.STDOUT + stdin_arg = subprocess.PIPE if self.enable_stdin else None self.proc = subprocess.Popen( self.cmd, + stdin=stdin_arg, stdout=subprocess.PIPE, stderr=stderr_arg, env=self.env, @@ -190,31 +195,45 @@ class ProcessManager: cleanup_attempts = 0 try: if self.proc: + # Terminate process group first so child tools don't survive parent exit. + if self.process_group_pid: + try: + os.killpg(self.process_group_pid, signal.SIGTERM) + except Exception: + pass + try: self.proc.terminate() - try: - self.proc.wait(timeout=timeout_terminate) - return - except subprocess.TimeoutExpired: - pass except Exception: pass + try: - self.proc.kill() - try: - self.proc.wait(timeout=timeout_kill) - return - except subprocess.TimeoutExpired: - pass + self.proc.wait(timeout=timeout_terminate) + except subprocess.TimeoutExpired: + pass except Exception: pass - # Kill entire process group (catches 7zz and other child processes) + + # Escalate to SIGKILL for stubborn children/process group. if self.process_group_pid: try: os.killpg(self.process_group_pid, signal.SIGKILL) except Exception: pass - # Last resort: pkill by command name + + try: + self.proc.kill() + except Exception: + pass + + try: + self.proc.wait(timeout=timeout_kill) + except subprocess.TimeoutExpired: + pass + except Exception: + pass + + # Last resort: pkill by command name (kept bounded). while cleanup_attempts < max_cleanup_attempts: try: subprocess.run(['pkill', '-f', os.path.basename(self.cmd[0])], timeout=5, capture_output=True) @@ -224,7 +243,7 @@ class ProcessManager: finally: # Always close pipes — unblocks threads blocked on read(1) or iterating stderr if self.proc: - for pipe in (self.proc.stdout, self.proc.stderr): + for pipe in (self.proc.stdin, self.proc.stdout, self.proc.stderr): if pipe: try: pipe.close() @@ -250,4 +269,20 @@ class ProcessManager: return self.proc.stdout.read(1) except (ValueError, OSError): return None - return None \ No newline at end of file + return None + + def write_stdin(self, line: str) -> bool: + """ + Write a line to the process stdin. Thread-safe. + Returns True on success, False if stdin is not available or process is gone. + """ + if not self.enable_stdin or not self.proc or not self.proc.stdin: + return False + with self._stdin_lock: + try: + payload = line if line.endswith('\n') else line + '\n' + self.proc.stdin.write(payload.encode()) + self.proc.stdin.flush() + return True + except (OSError, BrokenPipeError): + return False diff --git a/jackify/backend/handlers/ttw_installer_backend.py b/jackify/backend/handlers/ttw_installer_backend.py index 532a527..1b5a168 100644 --- a/jackify/backend/handlers/ttw_installer_backend.py +++ b/jackify/backend/handlers/ttw_installer_backend.py @@ -64,17 +64,29 @@ class TTWInstallerBackendMixin: stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1, universal_newlines=True ) + error_context: list = [] + capturing_explanation = False if process.stdout: for line in process.stdout: line = line.rstrip() if line: self.logger.info("TTW_Linux_Installer: %s", line) + lower = line.lower() + if 'failed' in lower or 'cannot continue' in lower or 'error:' in lower: + error_context.append(line.strip()) + capturing_explanation = True + elif capturing_explanation and line.startswith(' '): + error_context.append(line.strip()) + else: + capturing_explanation = False process.wait() ret = process.returncode if ret == 0: self.logger.info("TTW installation completed successfully.") return True, "TTW installation completed successfully!" self.logger.error("TTW installation process returned non-zero exit code: %s", ret) + if error_context: + return False, "TTW installation failed:\n" + "\n".join(error_context) return False, f"TTW installation failed with exit code {ret}" except Exception as e: self.logger.error("Error executing TTW_Linux_Installer: %s", e, exc_info=True) @@ -210,6 +222,8 @@ class TTWInstallerBackendMixin: stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1, universal_newlines=True ) + error_context: list = [] + capturing_explanation = False if process.stdout: for line in process.stdout: line = line.rstrip() @@ -217,12 +231,22 @@ class TTWInstallerBackendMixin: self.logger.info("TTW_Linux_Installer: %s", line) if output_callback: output_callback(line) + lower = line.lower() + if 'failed' in lower or 'cannot continue' in lower or 'error:' in lower: + error_context.append(line.strip()) + capturing_explanation = True + elif capturing_explanation and line.startswith(' '): + error_context.append(line.strip()) + else: + capturing_explanation = False process.wait() ret = process.returncode if ret == 0: self.logger.info("TTW installation completed successfully.") return True, "TTW installation completed successfully!" self.logger.error("TTW installation process returned non-zero exit code: %s", ret) + if error_context: + return False, "TTW installation failed:\n" + "\n".join(error_context) return False, f"TTW installation failed with exit code {ret}" except Exception as e: self.logger.error("Error executing TTW_Linux_Installer: %s", e, exc_info=True) diff --git a/jackify/backend/services/automated_prefix_service.py b/jackify/backend/services/automated_prefix_service.py index 2d1ef2f..f1ac03f 100644 --- a/jackify/backend/services/automated_prefix_service.py +++ b/jackify/backend/services/automated_prefix_service.py @@ -269,10 +269,8 @@ exit""" def get_ttw_installer_path() -> Optional[Path]: """Get path to TTW_Linux_Installer if available""" try: - from jackify.shared.paths import get_jackify_data_dir - ttw_path = get_jackify_data_dir() / "TTW_Linux_Installer" / "ttw_linux_gui" - if ttw_path.exists(): - return ttw_path + from .ttw_installer_service import get_ttw_installer_path + return get_ttw_installer_path() except Exception: pass return None @@ -405,4 +403,3 @@ exit""" return prefix_dir else: return None - diff --git a/jackify/backend/services/automated_prefix_workflow.py b/jackify/backend/services/automated_prefix_workflow.py index b1c874b..5cec4ed 100644 --- a/jackify/backend/services/automated_prefix_workflow.py +++ b/jackify/backend/services/automated_prefix_workflow.py @@ -47,11 +47,19 @@ class WorkflowMixin: startdir_matches = shortcut_startdir == modlist_install_dir if (name_matches and (exe_matches or startdir_matches)): + raw_appid = shortcut.get('appid') + normalized_appid = None + if raw_appid is not None: + try: + normalized_appid = str(int(raw_appid) & 0xFFFFFFFF) + except Exception: + normalized_appid = str(raw_appid) conflicts.append({ 'index': i, 'name': name, 'exe': shortcut_exe, - 'startdir': shortcut_startdir + 'startdir': shortcut_startdir, + 'appid': normalized_appid, }) if conflicts: @@ -124,42 +132,59 @@ class WorkflowMixin: Tuple of (success, prefix_path, appid, last_timestamp) """ logger.info("Starting proven working automated prefix creation workflow") - - # Show installation complete and configuration start headers FIRST - if progress_callback: - progress_callback("") - progress_callback("=" * 64) - progress_callback("= Installation phase complete =") - progress_callback("=" * 64) - progress_callback("") - progress_callback("=" * 64) - progress_callback("= Starting Configuration Phase =") - progress_callback("=" * 64) - progress_callback("") - - # Reset timing for Steam Integration section (part of Configuration Phase) - from jackify.shared.timing import start_new_phase - start_new_phase() - - # Show immediate feedback to user with section header - if progress_callback: - progress_callback("") # Blank line before Steam Integration - progress_callback("=== Steam Integration ===") - progress_callback(f"{self._get_progress_timestamp()} Creating Steam shortcut with native service") - - # Registry injection approach for both FNV and Enderal - from ..handlers.modlist_handler import ModlistHandler - modlist_handler = ModlistHandler() - special_game_type = modlist_handler.detect_special_game_type(modlist_install_dir) - # No launch options needed - FNV, FO3 and Enderal use registry injection - custom_launch_options = None - if special_game_type in ["fnv", "fo3", "enderal"]: - logger.info(f"Using registry injection approach for {special_game_type.upper()} modlist") - else: - logger.debug("Standard modlist - no special game handling needed") - try: + conflict_result = self.handle_existing_shortcut_conflict( + shortcut_name, + final_exe_path, + modlist_install_dir, + ) + if isinstance(conflict_result, list): + logger.warning( + "Found %d existing shortcut(s) with same name and path before Steam integration", + len(conflict_result), + ) + return ("CONFLICT", conflict_result, None, None) + if conflict_result is False: + logger.error("User cancelled due to shortcut conflict") + return False, None, None, None + + # Show installation complete and configuration start headers only after + # conflict checks pass, so users do not see Steam integration start + # messages when Jackify is about to stop for duplicate-shortcut review. + if progress_callback: + progress_callback("") + progress_callback("=" * 64) + progress_callback("= Installation phase complete =") + progress_callback("=" * 64) + progress_callback("") + progress_callback("=" * 64) + progress_callback("= Starting Configuration Phase =") + progress_callback("=" * 64) + progress_callback("") + + # Reset timing for Steam Integration section (part of Configuration Phase) + from jackify.shared.timing import start_new_phase + start_new_phase() + + # Show immediate feedback to user with section header + if progress_callback: + progress_callback("") # Blank line before Steam Integration + progress_callback("=== Steam Integration ===") + progress_callback(f"{self._get_progress_timestamp()} Creating Steam shortcut with native service") + + # Registry injection approach for both FNV and Enderal + from ..handlers.modlist_handler import ModlistHandler + modlist_handler = ModlistHandler() + special_game_type = modlist_handler.detect_special_game_type(modlist_install_dir) + + # No launch options needed - FNV, FO3 and Enderal use registry injection + custom_launch_options = None + if special_game_type in ["fnv", "fo3", "enderal"]: + logger.info(f"Using registry injection approach for {special_game_type.upper()} modlist") + else: + logger.debug("Standard modlist - no special game handling needed") + # Step 0: Shut down Steam before modifying VDF files # Required to safely modify shortcuts.vdf and config.vdf without race conditions logger.info("Step 0: Shutting down Steam before modifying VDF files") @@ -179,22 +204,6 @@ class WorkflowMixin: # Step 1: Create shortcut with native Steam service (Steam is now shut down) logger.info("Step 1: Creating shortcut with native Steam service") - - # DISABLED: Shortcut conflict detection temporarily disabled pending rework - # Re-enable after conflict resolution workflow refactor - # When re-enabled, this will detect and handle cases where shortcuts with the same - # name and path already exist in Steam, allowing users to resolve conflicts - # Disabled pending workflow improvements - planned for future release - # conflict_result = self.handle_existing_shortcut_conflict(shortcut_name, final_exe_path, modlist_install_dir) - # if isinstance(conflict_result, list): # Conflicts found - # logger.warning(f"Found {len(conflict_result)} existing shortcut(s) with same name and path") - # # Return a special tuple to indicate conflict that needs user resolution - # return ("CONFLICT", conflict_result, None) - # elif not conflict_result: # User cancelled or other failure - # logger.error("User cancelled due to shortcut conflict") - # return False, None, None, None - logger.info("Conflict detection temporarily disabled - proceeding with shortcut creation") - # Create shortcut using native Steam service with special game launch options success, appid = self.create_shortcut_with_native_service( shortcut_name, final_exe_path, modlist_install_dir, custom_launch_options, download_dir=download_dir @@ -387,4 +396,3 @@ class WorkflowMixin: if progress_callback: progress_callback(f"Error: {str(e)}") return False, None, None, None - diff --git a/jackify/backend/services/download_watcher_service.py b/jackify/backend/services/download_watcher_service.py new file mode 100644 index 0000000..815af11 --- /dev/null +++ b/jackify/backend/services/download_watcher_service.py @@ -0,0 +1,138 @@ +""" +Watches a directory for newly downloaded files and matches them against a +list of pending manual download items by lax filename comparison. +""" + +import os +import time +import logging +from dataclasses import dataclass, field +from pathlib import Path +from threading import Thread, Event +from typing import Callable, Optional + +logger = logging.getLogger(__name__) + +@dataclass +class WatcherConfig: + watch_directory: Path + watch_recursive: bool = False + debounce_seconds: float = 2.0 + additional_dirs: list = field(default_factory=list) + + +class DownloadWatcherService: + """ + Monitors a directory for files that match pending download items. + + Caller sets pending_items (list of dicts with at least 'file_name') and + registers an on_candidate callback that receives (Path, dict) when a + potential match is detected (after debounce, before hash validation). + """ + + def __init__(self, config: WatcherConfig, on_candidate: Callable[[Path, dict], None]): + self._config = config + self._on_candidate = on_candidate + self._pending_items: list[dict] = [] + self._pending_exact: list[tuple[str, dict]] = [] + self._stop_event = Event() + self._thread: Optional[Thread] = None + # Track known files so we only react to new/changed ones + self._known: dict[Path, float] = {} + + def set_pending_items(self, items: list[dict]) -> None: + """Replace the pending items list. Thread-safe for simple list swap.""" + self._pending_items = list(items) + self._pending_exact = [ + (str(item.get('file_name', '')).lower(), item) + for item in self._pending_items + if item.get('file_name') + ] + + def start(self) -> None: + if self._thread and self._thread.is_alive(): + return + self._stop_event.clear() + self._thread = Thread(target=self._watch_loop, daemon=True, name='DownloadWatcher') + self._thread.start() + logger.debug(f"Download watcher started on: {self._config.watch_directory}") + + def stop(self) -> None: + self._stop_event.set() + if self._thread: + self._thread.join(timeout=5) + logger.debug("Download watcher stopped") + + def _all_watch_dirs(self) -> list[Path]: + dirs = [self._config.watch_directory] + dirs.extend(self._config.additional_dirs) + return [d for d in dirs if d.is_dir()] + + def _scan(self) -> None: + for watch_dir in self._all_watch_dirs(): + try: + entries = list(watch_dir.iterdir()) if not self._config.watch_recursive else \ + [p for p in watch_dir.rglob('*') if p.is_file()] + for path in entries: + if not path.is_file(): + continue + # Skip browser temp files + if path.suffix in ('.part', '.crdownload', '.tmp'): + continue + try: + mtime = path.stat().st_mtime + except OSError: + continue + prev_mtime = self._known.get(path) + if prev_mtime == mtime: + continue + self._known[path] = mtime + self._check_candidate(path) + except OSError as e: + logger.debug(f"Watcher scan error on {watch_dir}: {e}") + + def _check_candidate(self, path: Path) -> None: + candidate_name = path.name.lower() + # Exact filename match (case-insensitive). + for expected_name, item in self._pending_exact: + if expected_name == candidate_name: + logger.debug(f"Candidate exact match: {path.name}") + self._debounce_and_emit(path, item) + return + # Some modlist metadata stores filenames with a leading dot that browsers + # strip when saving the download. Match against the stripped expected name. + for expected_name, item in self._pending_exact: + if expected_name.lstrip('.') == candidate_name: + logger.debug(f"Candidate dot-normalized match: {path.name} -> {expected_name}") + self._debounce_and_emit(path, item) + return + + def _debounce_and_emit(self, path: Path, item: dict) -> None: + def _wait_and_emit(): + prev_size = -1 + stable_count = 0 + needed = max(1, int(self._config.debounce_seconds / 0.5)) + for _ in range(needed * 4): # max ~2× debounce time + if self._stop_event.is_set(): + return + time.sleep(0.5) + try: + size = path.stat().st_size + except OSError: + return + if size == prev_size: + stable_count += 1 + if stable_count >= needed: + break + else: + stable_count = 0 + prev_size = size + if path.exists(): + self._on_candidate(path, item) + + Thread(target=_wait_and_emit, daemon=True, name=f'Debounce-{path.name[:20]}').start() + + def _watch_loop(self) -> None: + while not self._stop_event.is_set(): + self._scan() + self._stop_event.wait(timeout=1.0) diff --git a/jackify/backend/services/file_validator_service.py b/jackify/backend/services/file_validator_service.py new file mode 100644 index 0000000..9436deb --- /dev/null +++ b/jackify/backend/services/file_validator_service.py @@ -0,0 +1,261 @@ +""" +Hash validation and file move for manually downloaded archives. +Uses xxhash64 to match the engine's hash format exactly. +""" + +import struct +import shutil +import logging +from concurrent.futures import ThreadPoolExecutor +from dataclasses import dataclass +from pathlib import Path +from typing import Callable, Optional + +logger = logging.getLogger(__name__) + +# xxhash produces 16-char lowercase hex with no prefix - matches engine Hash.ToHex() +# C extension is ABI-locked to the Python version it was compiled against, so +# AppImage builds need a pure-Python fallback for cross-version compatibility. +try: + import xxhash + _XXHASH_IMPL = 'native' +except ImportError: + xxhash = None + _XXHASH_IMPL = 'fallback' + logger.info("xxhash C extension not available, using pure-Python fallback") + + +class _XXH64Fallback: + """Pure-Python xxhash64 implementation for when the C extension can't load. + Reference: https://github.com/Cyan4973/xxHash/blob/dev/doc/xxhash_spec.md""" + + _P1 = 11400714785074694791 + _P2 = 14029467366897019727 + _P3 = 1609587929392839161 + _P4 = 9650029242287828579 + _P5 = 2870177450012600261 + _M64 = 0xFFFFFFFFFFFFFFFF + + def __init__(self, seed: int = 0): + self._seed = seed & self._M64 + self._total_len = 0 + self._buf = b"" + self._v1 = (seed + self._P1 + self._P2) & self._M64 + self._v2 = (seed + self._P2) & self._M64 + self._v3 = seed & self._M64 + self._v4 = (seed - self._P1) & self._M64 + + @staticmethod + def _rotl64(x: int, r: int) -> int: + return ((x << r) | (x >> (64 - r))) & 0xFFFFFFFFFFFFFFFF + + def _round(self, acc: int, inp: int) -> int: + acc = (acc + inp * self._P2) & self._M64 + acc = self._rotl64(acc, 31) + acc = (acc * self._P1) & self._M64 + return acc + + def _merge_round(self, acc: int, val: int) -> int: + val = self._round(0, val) + acc ^= val + acc = (acc * self._P1 + self._P4) & self._M64 + return acc + + def update(self, data: bytes) -> None: + self._buf += data + self._total_len += len(data) + + if len(self._buf) < 32: + return + + p = 0 + end = len(self._buf) - 31 # process 32-byte blocks + + while p < end: + self._v1 = self._round(self._v1, struct.unpack_from(' str: + return format(self._digest(), '016x') + + def _digest(self) -> int: + M = self._M64 + if self._total_len >= 32: + h = self._rotl64(self._v1, 1) + h = (h + self._rotl64(self._v2, 7)) & M + h = (h + self._rotl64(self._v3, 12)) & M + h = (h + self._rotl64(self._v4, 18)) & M + h = self._merge_round(h, self._v1) + h = self._merge_round(h, self._v2) + h = self._merge_round(h, self._v3) + h = self._merge_round(h, self._v4) + else: + h = (self._seed + self._P5) & M + + h = (h + self._total_len) & M + + buf = self._buf + p = 0 + remaining = len(buf) + + while remaining >= 8: + k1 = struct.unpack_from('= 4: + k1 = struct.unpack_from(' 0: + h ^= (buf[p] * self._P5) & M + h = (self._rotl64(h, 11) * self._P1) & M + p += 1 + remaining -= 1 + + # Avalanche + h ^= h >> 33 + h = (h * self._P2) & M + h ^= h >> 29 + h = (h * self._P3) & M + h ^= h >> 32 + return h + +_CHUNK = 1024 * 1024 # 1 MB + + +def _reverse_hex_byte_order(hex_value: str) -> str: + """Reverse byte order of a hex string (e.g. aabbccdd -> ddccbbaa).""" + value = (hex_value or "").strip().lower() + if len(value) % 2 != 0: + return value + return "".join(reversed([value[i:i + 2] for i in range(0, len(value), 2)])) + + +def _hash_matches_expected(computed_hash: str, expected_hash: str) -> bool: + """Accept either canonical or byte-reversed xxhash64 representations.""" + computed = (computed_hash or "").strip().lower() + expected = (expected_hash or "").strip().lower() + if not computed or not expected: + return False + return computed == expected or _reverse_hex_byte_order(computed) == expected + + +@dataclass +class ValidationResult: + matches: bool + computed_hash: Optional[str] + file_path: Path + error: Optional[str] = None + + +class FileValidatorService: + """ + Validates downloaded files against expected xxhash64 and moves them to + the modlist downloads directory on success. + """ + + def __init__(self, max_workers: int = 2): + self._executor = ThreadPoolExecutor(max_workers=max_workers, thread_name_prefix='FileValidator') + + def validate_async( + self, + file_path: Path, + expected_hash: str, + modlist_download_dir: Path, + on_result: Callable[[ValidationResult, Optional[Path]], None], + dest_name: Optional[str] = None, + ) -> None: + """ + Validate file_path against expected_hash in a thread pool worker. + on_result(result, dest_path) is called on the worker thread when done. + dest_path is the moved file location if validation succeeded, else None. + dest_name overrides the destination filename (used when the engine's + canonical name differs from the downloaded file's name, e.g. leading dot). + """ + self._executor.submit( + self._validate_and_move, + file_path, expected_hash, modlist_download_dir, on_result, dest_name + ) + + def _validate_and_move( + self, + file_path: Path, + expected_hash: str, + modlist_download_dir: Path, + on_result: Callable, + dest_name: Optional[str] = None, + ) -> None: + result = self._validate(file_path, expected_hash) + dest: Optional[Path] = None + if result.matches: + try: + dest = self._move_file(file_path, modlist_download_dir, dest_name=dest_name) + logger.info( + "[MDL-1026] Archive move complete | " + f"source_path={file_path} destination_path={dest} hash={result.computed_hash or 'missing'}" + ) + except OSError as e: + logger.warning( + "[MDL-9020] Archive move failed after hash validation | " + f"source_path={file_path} destination_dir={modlist_download_dir} reason={e}" + ) + result = ValidationResult( + matches=False, + computed_hash=result.computed_hash, + file_path=file_path, + error=f"Move failed: {e}", + ) + on_result(result, dest) + + def _validate(self, file_path: Path, expected_hash: str) -> ValidationResult: + try: + # No expected hash — accept by filename match alone, just move the file. + if not (expected_hash or "").strip(): + return ValidationResult(matches=True, computed_hash=None, file_path=file_path) + h = xxhash.xxh64() if xxhash else _XXH64Fallback() + with open(file_path, 'rb') as f: + while True: + chunk = f.read(_CHUNK) + if not chunk: + break + h.update(chunk) + computed = h.hexdigest().lower() # 16-char lowercase hex, no prefix + matches = _hash_matches_expected(computed, expected_hash) + return ValidationResult( + matches=matches, + computed_hash=computed, + file_path=file_path, + ) + except OSError as e: + return ValidationResult(matches=False, computed_hash=None, file_path=file_path, + error=str(e)) + + def _move_file(self, source: Path, dest_dir: Path, dest_name: Optional[str] = None) -> Path: + dest_dir.mkdir(parents=True, exist_ok=True) + dest = dest_dir / (dest_name if dest_name else source.name) + # If the watched file is already in the modlist downloads directory, + # treat it as in-place and avoid a same-path move error. + try: + if source.resolve() == dest.resolve(): + logger.debug(f"Validated file already in modlist downloads directory: {source}") + return dest + except OSError: + pass + shutil.move(str(source), str(dest)) + logger.debug(f"Moved validated file: {source.name} -> {dest}") + return dest + + def shutdown(self) -> None: + self._executor.shutdown(wait=False) diff --git a/jackify/backend/services/manual_download_manager.py b/jackify/backend/services/manual_download_manager.py new file mode 100644 index 0000000..1eba569 --- /dev/null +++ b/jackify/backend/services/manual_download_manager.py @@ -0,0 +1,124 @@ +""" +Orchestrates the manual download workflow: +- Maintains queue of pending items +- Opens browser tabs (sliding window, N concurrent) +- Coordinates directory watcher and file validator +- Sends continue command to engine when all items are done +""" + +import logging +import threading +import time +from dataclasses import dataclass +from pathlib import Path +from typing import Callable, Literal, Optional + +from jackify.backend.services.download_watcher_service import DownloadWatcherService, WatcherConfig +from jackify.backend.services.file_validator_service import FileValidatorService +from jackify.backend.services.manual_download_manager_api_mixin import ManualDownloadManagerApiMixin +from jackify.backend.services.manual_download_manager_runtime_mixin import ManualDownloadManagerRuntimeMixin + +logger = logging.getLogger(__name__) + +STATUS = Literal["pending", "browser_opened", "validating", "complete", "deferred", "skipped", "error"] + +_STATE_FILE = Path.home() / '.local' / 'share' / 'jackify' / 'manual_download_state.json' + + +@dataclass +class DownloadItem: + file_name: str + nexus_url: str + expected_hash: str + expected_size: int + mod_name: str + mod_id: int = 0 + file_id: int = 0 + index: int = 0 + total: int = 0 + loop_iteration: int = 1 + status: STATUS = "pending" + local_path: Optional[str] = None + error_message: Optional[str] = None + needs_user_retry: bool = False + + @classmethod + def from_event(cls, evt: dict, loop_iteration: int = 1) -> 'DownloadItem': + # Engine historically emitted `nexus_url`, but manual-only/external sources + # may arrive as generic URL fields depending on engine version. + source_url = ( + evt.get('nexus_url') + or evt.get('download_url') + or evt.get('manual_url') + or evt.get('url') + or '' + ) + item = cls( + file_name=evt.get('file_name', ''), + nexus_url=source_url, + expected_hash=evt.get('expected_hash', ''), + expected_size=evt.get('expected_size', 0), + mod_name=evt.get('mod_name', evt.get('file_name', '')), + mod_id=evt.get('mod_id', 0), + file_id=evt.get('file_id', 0), + index=evt.get('index', 0), + total=evt.get('total', 0), + loop_iteration=loop_iteration, + ) + if not item.nexus_url: + # Engine contract says nexus_url should be present and non-empty. + # If missing, keep this item out of auto-open rotation and require + # explicit user attention/manual recovery. + item.needs_user_retry = True + item.error_message = "Malformed manual_download_required event: missing nexus_url" + return item + + +class ManualDownloadManager(ManualDownloadManagerApiMixin, ManualDownloadManagerRuntimeMixin): + """ + Manages the full manual download workflow for one engine session. + + Usage: + manager = ManualDownloadManager( + modlist_download_dir=Path(...), + watch_directory=Path(...), + concurrent_limit=2, + on_item_updated=my_callback, + on_send_continue=installer_thread.send_continue, + ) + manager.load_items(event_list, loop_iteration=1) + manager.start() + # ... user downloads files ... + # manager sends continue automatically when all done + manager.stop() + """ + + def __init__( + self, + modlist_download_dir: Path, + watch_directory: Path, + concurrent_limit: int = 2, + on_item_updated: Optional[Callable[[DownloadItem], None]] = None, + on_send_continue: Optional[Callable[[], None]] = None, + on_all_done: Optional[Callable[[int, int], None]] = None, + ): + self._dl_dir = modlist_download_dir + self._watch_dir = watch_directory + self._limit = max(1, min(5, concurrent_limit)) + self._on_item_updated = on_item_updated + self._on_send_continue = on_send_continue + self._on_all_done = on_all_done + + self._items: list[DownloadItem] = [] + self._lock = threading.Lock() + self._active_tabs = 0 + self._paused = False + self._started = False + self._startup_precheck_pending = 0 + self._run_id = f"mdl-{int(time.time())}-{id(self) % 10000}" + self._last_progress_log_completed = -1 + + additional = [modlist_download_dir] if modlist_download_dir != watch_directory else [] + config = WatcherConfig(watch_directory=watch_directory, additional_dirs=additional) + self._watcher = DownloadWatcherService(config, self._on_candidate) + self._validator = FileValidatorService(max_workers=2) diff --git a/jackify/backend/services/manual_download_manager_api_mixin.py b/jackify/backend/services/manual_download_manager_api_mixin.py new file mode 100644 index 0000000..14cb026 --- /dev/null +++ b/jackify/backend/services/manual_download_manager_api_mixin.py @@ -0,0 +1,163 @@ +from __future__ import annotations + +"""Public API methods for ManualDownloadManager.""" + +import json +from typing import Optional + + +class ManualDownloadManagerApiMixin: + """Mixin containing public manager API methods and status properties.""" + def load_items(self, events: list[dict], loop_iteration: int = 1) -> None: + """ + Merge a new batch of engine events into the existing item list. + + On loop_iteration > 1, engine only emits still-missing files. Items NOT + in the new batch that were pending are confirmed present by the engine + (they passed its rescan) and are marked complete. Genuinely new items + (edge case) are appended. active_tabs resets so the sliding window + opens fresh tabs for the remaining items. + """ + with self._lock: + existing_map = {item.file_name: item for item in self._items} + new_batch_names = {evt.get('file_name', '') for evt in events} + + # Items the engine confirmed are now present (not in new batch, were pending) + for item in self._items: + if item.file_name not in new_batch_names and item.status not in ('complete', 'deferred', 'skipped', 'error'): + item.status = 'complete' + item.needs_user_retry = False + + # Recheck loop: clear temporary defer state for still-missing files so they can + # re-enter active browser rotation in the new iteration. + if loop_iteration > 1: + for item in self._items: + if item.file_name in new_batch_names and item.status in ('deferred', 'skipped'): + item.status = 'pending' + item.needs_user_retry = False + item.error_message = None + + # Add items genuinely not seen before (first iteration, or edge case) + for evt in events: + name = evt.get('file_name', '') + if name not in existing_map: + # Local import avoids module-load circular dependency with manager class. + from jackify.backend.services.manual_download_manager import DownloadItem + + new_item = DownloadItem.from_event(evt, loop_iteration) + self._items.append(new_item) + if not new_item.nexus_url: + self._diag( + "MDL-9012", + "Engine manual-download event missing required nexus_url", + level="error", + file_name=new_item.file_name or "missing", + loop_iteration=loop_iteration, + mod_id=new_item.mod_id, + file_id=new_item.file_id, + ) + + self._active_tabs = 0 + total = len(self._items) + pending = sum(1 for i in self._items if i.status == 'pending') + complete = sum(1 for i in self._items if i.status == 'complete') + skipped = sum(1 for i in self._items if i.status == 'skipped') + sample_pending = [i.file_name for i in self._items if i.status == 'pending'][:5] + self._refresh_watcher_pending_items() + self._diag( + "MDL-1001", + "Manual download batch loaded", + loop_iteration=loop_iteration, + batch_size=len(events), + total_items=total, + pending=pending, + complete=complete, + skipped=skipped, + pending_sample=json.dumps(sample_pending, ensure_ascii=True), + ) + + def start(self) -> None: + with self._lock: + if self._started: + return + self._started = True + self._diag( + "MDL-1002", + "Manual download watcher started", + watch_dir=str(self._watch_dir), + downloads_dir=str(self._dl_dir), + concurrent_limit=self._limit, + ) + self._watcher.start() + matched = self._ingest_existing_files() + with self._lock: + self._startup_precheck_pending = matched + if matched: + self._diag("MDL-1003", "Pre-existing archives detected", matched=matched) + self._diag("MDL-1016", "Deferring tab opening until precheck validation completes", pending_precheck=matched) + else: + self._open_next_tabs() + + def stop(self) -> None: + self._watcher.stop() + self._validator.shutdown() + with self._lock: + self._started = False + self._startup_precheck_pending = 0 + self._diag("MDL-1009", "Manual download manager stopped") + + def pause(self) -> None: + with self._lock: + self._paused = True + + def resume(self) -> None: + with self._lock: + self._paused = False + self._diag("MDL-1008", "Manual download resumed") + # Explicit user start/resume must open tabs even if startup precheck + # bookkeeping is still in-flight. + self._open_next_tabs(force_user_start=True) + + def skip_item(self, file_name: str) -> None: + item_to_notify: Optional[DownloadItem] = None + with self._lock: + for item in self._items: + if item.file_name == file_name and item.status not in ('complete',): + item.status = 'deferred' + if self._active_tabs > 0: + self._active_tabs -= 1 + item_to_notify = item + break + if item_to_notify is not None: + self._notify(item_to_notify) + self._open_next_tabs() + self._check_all_done() + + def set_concurrent_limit(self, limit: int) -> None: + with self._lock: + self._limit = max(1, min(5, limit)) + applied = self._limit + started = self._started + self._diag("MDL-1006", "Manual download concurrency updated", concurrent_limit=applied) + if started: + self._open_next_tabs() + + @property + def items(self) -> list[DownloadItem]: + with self._lock: + return list(self._items) + + @property + def pending_count(self) -> int: + with self._lock: + return sum(1 for i in self._items if i.status == 'pending') + + @property + def complete_count(self) -> int: + with self._lock: + return sum(1 for i in self._items if i.status == 'complete') + + @property + def skipped_count(self) -> int: + with self._lock: + return sum(1 for i in self._items if i.status in ('deferred', 'skipped')) diff --git a/jackify/backend/services/manual_download_manager_runtime_mixin.py b/jackify/backend/services/manual_download_manager_runtime_mixin.py new file mode 100644 index 0000000..d0f8b7e --- /dev/null +++ b/jackify/backend/services/manual_download_manager_runtime_mixin.py @@ -0,0 +1,479 @@ +from __future__ import annotations + +"""Internal runtime methods for ManualDownloadManager.""" + +import json +import logging +import subprocess +from pathlib import Path +from typing import Optional + +from jackify.backend.services.file_validator_service import ValidationResult + +logger = logging.getLogger(__name__) + + +class ManualDownloadManagerRuntimeMixin: + """Mixin containing browser/watcher/validation runtime methods.""" + def _open_next_tabs(self, force_user_start: bool = False) -> None: + to_open = [] + to_notify = [] + with self._lock: + if not self._started or self._paused: + return + if self._startup_precheck_pending > 0 and not force_user_start: + return + while self._active_tabs < self._limit: + item = self._next_pending(include_retry=force_user_start) + if item is None: + break + if force_user_start and item.needs_user_retry: + item.needs_user_retry = False + item.error_message = None + item.status = 'browser_opened' + self._active_tabs += 1 + to_notify.append(item) + to_open.append(item) + active_tabs = self._active_tabs + pending_left = sum(1 for i in self._items if i.status == 'pending') + if to_open: + self._diag( + "MDL-1010", + "Opening next manual download tab window", + opening_count=len(to_open), + active_tabs=active_tabs, + pending_after_schedule=pending_left, + ) + # Notify outside the lock to prevent GUI callbacks from re-entering manager state. + for item in to_notify: + self._notify(item) + # Open browser tabs outside the lock so Popen/fork doesn't stall lock holders + for item in to_open: + opened, error_message = self._open_browser(item) + if opened: + continue + item_to_notify: Optional[DownloadItem] = None + with self._lock: + # Revert failed launch so the row does not falsely remain "Browser Opened". + current = self._item_by_name(item.file_name) + if current and current.status == 'browser_opened': + current.status = 'pending' + if self._active_tabs > 0: + self._active_tabs -= 1 + current.error_message = error_message + if error_message and "No URL available" in error_message: + current.needs_user_retry = True + item_to_notify = current + if item_to_notify is not None: + self._notify(item_to_notify) + self._diag( + "MDL-9001", + "Automatic browser launch failed for manual download item", + level="warning", + file_name=item.file_name, + reason=error_message or "unknown launcher failure", + ) + + def _next_pending(self, include_retry: bool = False) -> Optional[DownloadItem]: + for item in self._items: + if item.status != 'pending': + continue + if item.needs_user_retry and not include_retry: + continue + return item + return None + + def _open_browser(self, item: DownloadItem) -> tuple[bool, Optional[str]]: + url = item.nexus_url + if not url: + msg = "No URL available for manual download item" + logger.warning(f"{msg}: {item.file_name}") + return False, msg + + # Linux desktop launch fallbacks. xdg-open should cover most environments, + # but keep alternates for distributions where handlers differ. + launch_cmds = ( + ['xdg-open', url], + ['gio', 'open', url], + ['sensible-browser', url], + ) + + launch_errors: list[str] = [] + for cmd in launch_cmds: + try: + proc = subprocess.Popen( + cmd, + stdout=subprocess.DEVNULL, + stderr=subprocess.PIPE, + start_new_session=True, + ) + except OSError as e: + launch_errors.append(f"{cmd[0]} not available: {e}") + continue + + try: + rc = proc.wait(timeout=3) + except subprocess.TimeoutExpired: + # Launcher still running after handoff window; treat as success. + logger.debug(f"Opened browser for: {item.file_name} via {cmd[0]}") + return True, None + + if rc == 0: + logger.debug(f"Opened browser for: {item.file_name} via {cmd[0]}") + return True, None + + stderr_tail = "" + try: + stderr_tail = (proc.stderr.read() or b"").decode("utf-8", errors="replace").strip() + except Exception: + stderr_tail = "" + launch_errors.append(f"{cmd[0]} exited {rc}{(': ' + stderr_tail) if stderr_tail else ''}") + + msg = f"Could not open browser automatically for {item.file_name}" + logger.error(f"{msg}. Launch attempts: {' | '.join(launch_errors)}") + return False, msg + + def _on_candidate(self, path: Path, hint: dict, from_startup_precheck: bool = False) -> bool: + """Called by watcher after debounce when a potential match is found.""" + file_name = hint.get('file_name', '') + item_to_notify: Optional[DownloadItem] = None + reject_reason = "" + had_browser_slot = False + with self._lock: + item = self._item_by_name(file_name) + if item is None: + reject_reason = "unknown_item" + elif item.status in ('complete', 'skipped'): + reject_reason = f"terminal_status:{item.status}" + elif item.status == 'validating': + reject_reason = "already_validating" + if reject_reason: + self._diag( + "MDL-1022", + "Candidate ignored", + file_name=file_name or "missing", + source_path=str(path), + from_precheck=from_startup_precheck, + reason=reject_reason, + ) + return False + had_browser_slot = item.status == 'browser_opened' + item.status = 'validating' + item_to_notify = item + + if item_to_notify is not None: + self._notify(item_to_notify) + self._diag( + "MDL-1020", + "Candidate queued for validation", + file_name=file_name or "missing", + source_path=str(path), + from_precheck=from_startup_precheck, + ) + + # Pass the engine's canonical filename as dest_name so that if the browser + # stripped a leading dot, the file is renamed correctly on move. + canonical_name = hint.get('file_name') or None + dest_name = canonical_name if canonical_name and canonical_name != path.name else None + self._validator.validate_async( + file_path=path, + expected_hash=hint.get('expected_hash', ''), + modlist_download_dir=self._dl_dir, + on_result=lambda result, dest: self._on_validation_result( + file_name, + result, + dest, + from_startup_precheck=from_startup_precheck, + had_browser_slot=had_browser_slot, + ), + dest_name=dest_name, + ) + return True + + def _on_validation_result( + self, + file_name: str, + result: ValidationResult, + dest: Optional[Path], + from_startup_precheck: bool = False, + had_browser_slot: bool = False, + ) -> None: + item_to_notify: Optional[DownloadItem] = None + validation_failed = False + completed_now = False + precheck_ready = False + expected_hash = "" + mod_id = 0 + file_id = 0 + source_path = str(result.file_path) if getattr(result, "file_path", None) else "" + computed_hash = (result.computed_hash or "").lower() if result.computed_hash else "" + with self._lock: + item = self._item_by_name(file_name) + if item is None: + return + expected_hash = (item.expected_hash or "").lower() + mod_id = item.mod_id + file_id = item.file_id + if result.matches and dest: + item.status = 'complete' + item.local_path = str(dest) + item.needs_user_retry = False + if had_browser_slot and self._active_tabs > 0: + self._active_tabs -= 1 + item_to_notify = item + completed_now = True + else: + # Hash mismatch or validation error — revert to pending so the + # sliding window can re-open a browser tab and the watcher can + # re-validate if the user downloads the correct file. + item.status = 'pending' + msg = result.error or f"Hash mismatch (got {result.computed_hash})" + item.error_message = msg + logger.warning(f"Validation failed for {file_name}: {msg}") + if had_browser_slot and self._active_tabs > 0: + self._active_tabs -= 1 + item_to_notify = item + validation_failed = True + if from_startup_precheck and self._startup_precheck_pending > 0: + self._startup_precheck_pending -= 1 + precheck_ready = self._startup_precheck_pending == 0 + + if item_to_notify is not None: + self._notify(item_to_notify) + if completed_now: + self._diag( + "MDL-1021", + "Archive validated and accepted", + file_name=file_name, + source_path=source_path or "missing", + destination_path=str(dest) if dest else "missing", + expected_hash=expected_hash or "missing", + computed_hash=computed_hash or "missing", + from_precheck=from_startup_precheck, + mod_id=mod_id, + file_id=file_id, + ) + self._maybe_log_progress_summary() + if precheck_ready: + self._diag("MDL-1017", "Startup precheck validation complete; opening tabs") + self._open_next_tabs() + if validation_failed: + self._refresh_watcher_pending_items() + if not from_startup_precheck: + self._open_next_tabs() + self._diag( + "MDL-9002", + "Archive validation failed", + level="warning", + file_name=file_name, + expected_hash=expected_hash or "missing", + computed_hash=computed_hash or "missing", + source_path=source_path or "missing", + mod_id=mod_id, + file_id=file_id, + from_precheck=from_startup_precheck, + reason=result.error or "hash mismatch", + ) + return + + # Update watcher pending list (remove completed item, keep other in-flight items). + self._refresh_watcher_pending_items() + if not from_startup_precheck: + self._open_next_tabs() + self._check_all_done() + + def _check_all_done(self) -> None: + with self._lock: + remaining = [i for i in self._items if i.status not in ('complete', 'deferred', 'skipped', 'error')] + if remaining: + return + completed = sum(1 for i in self._items if i.status == 'complete') + skipped = sum(1 for i in self._items if i.status in ('deferred', 'skipped')) + + self._diag("MDL-1011", "Manual download phase completed", completed=completed, skipped=skipped) + if self._on_all_done: + self._on_all_done(completed, skipped) + if self._on_send_continue: + self._diag("MDL-1012", "Sending continue command to engine") + self._on_send_continue() + + def _item_by_name(self, file_name: str) -> Optional[DownloadItem]: + for item in self._items: + if item.file_name == file_name: + return item + return None + + def _refresh_watcher_pending_items(self) -> None: + """Keep watcher tracking all non-terminal items, not only pure 'pending' ones.""" + with self._lock: + pending_items = [ + {'file_name': i.file_name, 'expected_hash': i.expected_hash, 'expected_size': i.expected_size} + for i in self._items + if i.status not in ('complete', 'error') + ] + pending_count = len(pending_items) + sample_pending = [i['file_name'] for i in pending_items[:5]] + self._watcher.set_pending_items(pending_items) + self._diag( + "MDL-1019", + "Watcher pending list refreshed", + pending_count=pending_count, + pending_sample=json.dumps(sample_pending, ensure_ascii=True), + ) + + def _ingest_existing_files(self) -> int: + """ + Pre-check watch/modlist directories for already-present archives so users + do not need to re-download files that already exist. + """ + dirs: list[Path] = [] + if self._watch_dir.is_dir(): + dirs.append(self._watch_dir) + if self._dl_dir.is_dir() and self._dl_dir != self._watch_dir: + dirs.append(self._dl_dir) + if not dirs: + return 0 + + existing_files: list[Path] = [] + for d in dirs: + try: + for p in d.iterdir(): + if p.is_file() and p.suffix not in ('.part', '.crdownload', '.tmp'): + existing_files.append(p) + except OSError as e: + logger.warning(f"[MDL-9021] Precheck scan error: dir={d} reason={e}") + continue + + if not existing_files: + self._diag("MDL-1023", "Startup precheck found no candidate files", scan_dirs=len(dirs)) + return 0 + + exact_map: dict[str, Path] = {} + for p in existing_files: + exact_map.setdefault(p.name.lower(), p) + + with self._lock: + targets = [ + {'file_name': i.file_name, 'expected_hash': i.expected_hash, 'expected_size': i.expected_size} + for i in self._items + if i.status not in ('complete', 'error') + ] + + self._diag( + "MDL-1024", + "Startup precheck scan summary", + scan_dirs=len(dirs), + discovered_files=len(existing_files), + pending_targets=len(targets), + discovered_sample=json.dumps([p.name for p in existing_files[:5]], ensure_ascii=True), + target_sample=json.dumps([t.get('file_name', '') for t in targets[:5]], ensure_ascii=True), + ) + + matched = 0 + used_paths: set[Path] = set() + for hint in targets: + name = hint['file_name'] + exact = exact_map.get(name.lower()) + if exact is None: + # Leading-dot normalization: browser may strip a leading dot that + # the engine uses in its canonical filename. + stripped = name.lower().lstrip('.') + if stripped != name.lower(): + exact = exact_map.get(stripped) + if exact is None or exact in used_paths: + continue + used_paths.add(exact) + if self._on_candidate(exact, hint, from_startup_precheck=True): + matched += 1 + + if matched: + logger.info(f"[MDL-1025] Startup precheck queued {matched} archive(s) for validation") + else: + self._diag("MDL-1025", "Startup precheck found zero exact filename matches") + return matched + + def reopen_item(self, file_name: str) -> bool: + """Re-open a specific item's URL (e.g. if user closed browser tab accidentally).""" + notify_item: Optional[DownloadItem] = None + with self._lock: + item = self._item_by_name(file_name) + if item is None: + return False + if item.status in ('complete', 'skipped'): + return False + if item.status != 'browser_opened': + item.status = 'browser_opened' + item.needs_user_retry = False + self._active_tabs += 1 + notify_item = item + if notify_item is not None: + self._notify(notify_item) + + if item is None: + return False + opened, error = self._open_browser(item) + if not opened: + revert_item: Optional[DownloadItem] = None + with self._lock: + current = self._item_by_name(file_name) + if current is not None and current.status == 'browser_opened': + current.status = 'pending' + current.needs_user_retry = True + if self._active_tabs > 0: + self._active_tabs -= 1 + revert_item = current + if revert_item is not None: + self._notify(revert_item) + self._diag( + "MDL-9011", + "Manual reopen failed", + level="warning", + file_name=file_name, + reason=error or "unknown launcher failure", + ) + return False + self._diag("MDL-1018", "Manual item URL re-opened by user", file_name=file_name) + return True + + def _maybe_log_progress_summary(self) -> None: + with self._lock: + complete = sum(1 for i in self._items if i.status == 'complete') + skipped = sum(1 for i in self._items if i.status in ('deferred', 'skipped')) + pending = sum(1 for i in self._items if i.status == 'pending') + validating = sum(1 for i in self._items if i.status == 'validating') + opened = sum(1 for i in self._items if i.status == 'browser_opened') + total = len(self._items) + if complete == self._last_progress_log_completed: + return + if complete in (1, total) or complete % 10 == 0: + self._last_progress_log_completed = complete + self._diag( + "MDL-1013", + "Manual download progress summary", + total=total, + complete=complete, + browser_opened=opened, + validating=validating, + pending=pending, + skipped=skipped, + needs_retry=sum(1 for i in self._items if i.needs_user_retry), + ) + + def _diag(self, code: str, message: str, level: str = "info", **ctx) -> None: + details = " ".join(f"{k}={v}" for k, v in ctx.items()) + text = f"[{code}] run={self._run_id} {message}" + if details: + text = f"{text} | {details}" + if level == "warning": + logger.warning(text) + elif level == "error": + logger.error(text) + else: + logger.info(text) + + def _notify(self, item: DownloadItem) -> None: + if self._on_item_updated: + try: + self._on_item_updated(item) + except Exception as e: + logger.debug(f"on_item_updated callback error: {e}") diff --git a/jackify/backend/services/mo2_setup_service.py b/jackify/backend/services/mo2_setup_service.py index b2a24bc..1dd86bb 100644 --- a/jackify/backend/services/mo2_setup_service.py +++ b/jackify/backend/services/mo2_setup_service.py @@ -11,6 +11,8 @@ import re import shutil import logging import subprocess +import tempfile +import time from pathlib import Path from typing import Callable, Optional, Tuple @@ -31,10 +33,55 @@ class MO2SetupService: GITHUB_API = "https://api.github.com/repos/ModOrganizer2/modorganizer/releases/latest" ASSET_PATTERN = re.compile(r"Mod\.Organizer-\d+\.\d+(\.\d+)?\.7z$") + def _extract_archive( + self, + archive_path: Path, + install_dir: Path, + should_cancel: Optional[Callable[[], bool]] = None, + ) -> Tuple[bool, Optional[str]]: + """Extract the MO2 archive without interactive prompts and honor cancellation.""" + + process = None + try: + process = subprocess.Popen( + ['7z', 'x', '-y', '-aoa', str(archive_path), f'-o{install_dir}'], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + + while True: + if should_cancel and should_cancel(): + process.terminate() + try: + process.wait(timeout=5) + except subprocess.TimeoutExpired: + process.kill() + process.wait(timeout=5) + return False, "MO2 setup cancelled." + + returncode = process.poll() + if returncode is not None: + stdout, stderr = process.communicate() + if returncode != 0: + err = (stderr or stdout or "").strip() + return False, f"Extraction failed: {err or '7z returned a non-zero exit code.'}" + return True, None + + time.sleep(0.1) + except Exception as e: + if process is not None: + try: + process.kill() + except Exception: + pass + return False, f"Extraction failed: {e}" + def setup_mo2( self, install_dir: Path, shortcut_name: str = "Mod Organizer 2", + existing_appid: Optional[int] = None, progress_callback: Optional[Callable[[str], None]] = None, should_cancel: Optional[Callable[[], bool]] = None, ) -> Tuple[bool, Optional[int], Optional[str]]: @@ -88,16 +135,21 @@ class MO2SetupService: return False, None, "Could not find main MO2 .7z asset in latest release." # Download - archive_path = install_dir / asset['name'] _progress(f"Downloading {asset['name']}...") if _cancel_requested(): return False, None, "MO2 setup cancelled." try: + with tempfile.NamedTemporaryFile(prefix="jackify-mo2-", suffix=".7z", delete=False) as tmp_file: + archive_path = Path(tmp_file.name) with requests.get(asset['browser_download_url'], stream=True, timeout=120, verify=True) as r: r.raise_for_status() with open(archive_path, 'wb') as f: for chunk in r.iter_content(chunk_size=8192): if _cancel_requested(): + try: + archive_path.unlink(missing_ok=True) + except Exception: + pass return False, None, "MO2 setup cancelled." f.write(chunk) except Exception as e: @@ -107,18 +159,13 @@ class MO2SetupService: _progress(f"Extracting to {install_dir}...") if _cancel_requested(): return False, None, "MO2 setup cancelled." - try: - result = subprocess.run( - ['7z', 'x', str(archive_path), f'-o{install_dir}'], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - timeout=1200, - ) - if result.returncode != 0: - err = result.stderr.decode(errors='ignore') - return False, None, f"Extraction failed: {err}" - except Exception as e: - return False, None, f"Extraction failed: {e}" + extract_ok, extract_error = self._extract_archive(archive_path, install_dir, should_cancel) + if not extract_ok: + try: + archive_path.unlink(missing_ok=True) + except Exception: + pass + return False, None, extract_error # Validate mo2_exe = install_dir / "ModOrganizer.exe" @@ -149,12 +196,22 @@ class MO2SetupService: try: from .automated_prefix_service import AutomatedPrefixService svc = AutomatedPrefixService() - success, prefix_path, app_id, _last_ts = svc.run_working_workflow( - shortcut_name=shortcut_name, - modlist_install_dir=str(install_dir), - final_exe_path=str(mo2_exe), - progress_callback=_progress, - ) + if existing_appid is not None: + app_id = int(existing_appid) + _progress(f"Reusing existing Steam shortcut with AppID: {app_id}") + prefix_path = svc.get_prefix_path(app_id) + if prefix_path is None: + if not svc.create_prefix_with_proton_wrapper(app_id): + return False, None, "Failed to create Proton prefix for existing shortcut." + prefix_path = svc.get_prefix_path(app_id) + success = True + else: + success, prefix_path, app_id, _last_ts = svc.run_working_workflow( + shortcut_name=shortcut_name, + modlist_install_dir=str(install_dir), + final_exe_path=str(mo2_exe), + progress_callback=_progress, + ) except Exception as e: logger.error(f"AutomatedPrefixService failed: {e}") return False, None, f"Prefix setup failed: {e}" diff --git a/jackify/backend/services/modlist_service.py b/jackify/backend/services/modlist_service.py index f15cef8..724cbce 100644 --- a/jackify/backend/services/modlist_service.py +++ b/jackify/backend/services/modlist_service.py @@ -334,9 +334,9 @@ class ModlistService(ModlistServiceInstallationMixin): if completion_callback: if success: - debug_callback("Configuration completed successfully, calling completion callback") + debug_callback("Core configuration complete, calling completion callback") # Pass ENB detection status through callback - completion_callback(True, "Configuration completed successfully!", context.name, enb_detected) + completion_callback(True, "Core configuration complete", context.name, enb_detected) else: debug_callback("Configuration failed, calling completion callback with failure") completion_callback(False, "Configuration failed", context.name, False) @@ -439,7 +439,7 @@ class ModlistService(ModlistServiceInstallationMixin): if success: logger.info("Modlist configuration completed successfully") if completion_callback: - completion_callback(True, "Configuration completed successfully", context.name, False) + completion_callback(True, "Core configuration complete", context.name, False) else: logger.warning("Modlist configuration had issues") if completion_callback: diff --git a/jackify/backend/services/modlist_service_installation.py b/jackify/backend/services/modlist_service_installation.py index e1d1b00..e63eaef 100644 --- a/jackify/backend/services/modlist_service_installation.py +++ b/jackify/backend/services/modlist_service_installation.py @@ -186,10 +186,23 @@ class ModlistServiceInstallationMixin: clean_env = get_clean_subprocess_env() proc = subprocess.Popen( - cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=False, env=clean_env, cwd=engine_dir ) + def _write_stdin(line: str) -> bool: + try: + payload = line if line.endswith('\n') else line + '\n' + proc.stdin.write(payload.encode()) + proc.stdin.flush() + return True + except (OSError, BrokenPipeError): + return False + + from jackify.backend.utils.cc_content_detector import is_cc_content_error, extract_cc_filename + import json as _json + _cc_filename = None + _pending_manual: list = [] buffer = b'' while True: chunk = proc.stdout.read(1) @@ -197,26 +210,81 @@ class ModlistServiceInstallationMixin: break buffer += chunk - if chunk == b'\n': + if chunk in (b'\n', b'\r'): line = buffer.decode('utf-8', errors='replace') - if output_callback: - output_callback(line.rstrip()) + decoded = line.rstrip() buffer = b'' - elif chunk == b'\r': - line = buffer.decode('utf-8', errors='replace') + + # JSON engine events - handle silently, don't pass to output_callback + if decoded.strip().startswith('{'): + try: + obj = _json.loads(decoded.strip()) + event = obj.get('event') + if event == 'manual_download_required': + _pending_manual.append(obj) + continue + if event == 'manual_download_list_complete': + loop_iter = obj.get('loop_iteration', 1) + for item in _pending_manual: + item['loop_iteration'] = loop_iter + items_batch = list(_pending_manual) + _pending_manual.clear() + 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 + manual_limit = max(1, min(5, manual_limit)) + from jackify.frontends.cli.commands.manual_download_flow import run_cli_manual_download_phase + completed = run_cli_manual_download_phase( + events=items_batch, + loop_iteration=loop_iter, + download_dir=actual_download_path, + stdin_write=_write_stdin, + output_callback=output_callback, + concurrent_limit=manual_limit, + ) + if not completed: + if proc.poll() is None: + proc.terminate() + break + continue + if event == 'manual_download_phase_complete': + if output_callback: + found = obj.get('total_found', 0) + required = obj.get('total_required', 0) + output_callback(f"All manual downloads confirmed ({found}/{required}). Resuming installation...") + continue + except (_json.JSONDecodeError, ValueError): + pass + if output_callback: - output_callback(line.rstrip()) - buffer = b'' + output_callback(decoded) + if _cc_filename is None and is_cc_content_error(decoded): + _cc_filename = extract_cc_filename(decoded) or "" if buffer: line = buffer.decode('utf-8', errors='replace') + decoded = line.rstrip() if output_callback: - output_callback(line.rstrip()) + output_callback(decoded) + if _cc_filename is None and is_cc_content_error(decoded): + _cc_filename = extract_cc_filename(decoded) or "" proc.wait() if proc.returncode != 0: if output_callback: output_callback(f"Jackify Install Engine exited with code {proc.returncode}.") + if _cc_filename is not None and output_callback: + fname_note = f" ({_cc_filename})" if _cc_filename else "" + output_callback("") + output_callback(f"[WARN] Anniversary Edition Content Missing{fname_note}") + output_callback(" - Open Vanilla Skyrim SE/AE and let it run until all Creation Club content has downloaded.") + output_callback(" - From the Skyrim main menu, go into Creations and select 'Download All'.") + output_callback(" - If specific files are still missing, search for and download them from the Creations menu.") + output_callback(" - If problems persist, uninstall and reinstall Skyrim, then launch once to trigger the AE download.") + output_callback(" - Note: Skyrim AE via Steam Family Sharing does not transfer DLC content.") return False if output_callback: output_callback("Installation completed successfully") diff --git a/jackify/backend/services/native_steam_service.py b/jackify/backend/services/native_steam_service.py index a360dc7..3779709 100644 --- a/jackify/backend/services/native_steam_service.py +++ b/jackify/backend/services/native_steam_service.py @@ -16,6 +16,7 @@ from pathlib import Path from typing import Optional, Tuple, Dict, Any, List from ..handlers.vdf_handler import VDFHandler +from jackify.shared.steam_utils import get_ordered_steam_roots, STEAM_PREFERENCE_AUTO logger = logging.getLogger(__name__) @@ -30,13 +31,14 @@ class NativeSteamService: """ def __init__(self): - self.steam_paths = [ - Path.home() / ".steam" / "steam", - Path.home() / ".local" / "share" / "Steam", - Path.home() / ".var" / "app" / "com.valvesoftware.Steam" / "data" / "Steam", - Path.home() / ".var" / "app" / "com.valvesoftware.Steam" / ".local" / "share" / "Steam", - Path.home() / ".var" / "app" / "com.valvesoftware.Steam" / "home" / ".local" / "share" / "Steam" - ] + preference = STEAM_PREFERENCE_AUTO + try: + from jackify.backend.handlers.config_handler import ConfigHandler + + preference = ConfigHandler().get("steam_install_preference", STEAM_PREFERENCE_AUTO) + except Exception: + pass + self.steam_paths = get_ordered_steam_roots(preference=preference) self.steam_path = None self.userdata_path = None self.user_id = None @@ -620,4 +622,4 @@ class NativeSteamService: except Exception as e: logger.error(f"Error creating symlink: {e}") - return False \ No newline at end of file + return False diff --git a/jackify/backend/services/nexus_oauth_protocol.py b/jackify/backend/services/nexus_oauth_protocol.py index 904930b..def7ea4 100644 --- a/jackify/backend/services/nexus_oauth_protocol.py +++ b/jackify/backend/services/nexus_oauth_protocol.py @@ -26,6 +26,7 @@ class NexusOAuthProtocolMixin: 'APPIMAGE' in env or 'APPDIR' in env or (sys.argv[0] and sys.argv[0].endswith('.AppImage')) ) + exec_path_reliable = True if is_appimage: if 'APPIMAGE' in env: exec_path = env['APPIMAGE'] @@ -35,34 +36,27 @@ class NexusOAuthProtocolMixin: logger.info("Using resolved sys.argv[0]: %s", exec_path) else: exec_path = sys.argv[0] + exec_path_reliable = False logger.warning("Using sys.argv[0] as fallback: %s", exec_path) else: src_dir = Path(__file__).resolve().parent.parent.parent.parent exec_path = f'bash -c \'cd "{src_dir}" && "{sys.executable}" -m jackify.frontends.gui "$@"\' --' logger.info("DEV mode exec path: %s", exec_path) logger.info("Source directory: %s", src_dir) - needs_update = False - if not desktop_file.exists(): - needs_update = True - logger.info("Creating desktop file for protocol handler") - else: + + expected_exec = f'Exec="{exec_path}" %u' if is_appimage else f'Exec={exec_path} %u' + needs_write = not desktop_file.exists() + if not needs_write and exec_path_reliable: current_content = desktop_file.read_text() - if is_appimage: - expected_exec = f'Exec="{exec_path}" %u' - else: - expected_exec = f"Exec={exec_path} %u" if expected_exec not in current_content: - needs_update = True - logger.info("Updating desktop file with new Exec path: %s", exec_path) - if is_appimage and ' ' in exec_path: - import re - if re.search(r'Exec=[^"]\S*\s+\S*\.AppImage', current_content): - needs_update = True - logger.info("Fixing malformed desktop file (unquoted path with spaces)") - if needs_update: - desktop_file.parent.mkdir(parents=True, exist_ok=True) - if is_appimage: - desktop_content = f"""[Desktop Entry] + needs_write = True + logger.info("Desktop file Exec path outdated, updating: %s", exec_path) + elif not needs_write and not exec_path_reliable: + logger.warning("Could not reliably determine AppImage path, keeping existing desktop file") + + desktop_file.parent.mkdir(parents=True, exist_ok=True) + if needs_write and is_appimage: + desktop_content = f"""[Desktop Entry] Type=Application Name=Jackify Comment=Wabbajack modlist manager for Linux @@ -72,9 +66,9 @@ Terminal=false Categories=Game;Utility; MimeType=x-scheme-handler/jackify; """ - else: - src_dir = Path(__file__).resolve().parent.parent.parent.parent - desktop_content = f"""[Desktop Entry] + elif needs_write: + src_dir = Path(__file__).resolve().parent.parent.parent.parent + desktop_content = f"""[Desktop Entry] Type=Application Name=Jackify Comment=Wabbajack modlist manager for Linux @@ -85,10 +79,14 @@ Categories=Game;Utility; MimeType=x-scheme-handler/jackify; Path={src_dir} """ + if needs_write: desktop_file.write_text(desktop_content) logger.info("Desktop file written: %s", desktop_file) logger.info("Exec path: %s", exec_path) logger.info("AppImage mode: %s", is_appimage) + else: + logger.debug("Desktop file up to date, skipping write") + logger.info("Registering jackify:// protocol handler") apps_dir = Path.home() / ".local" / "share" / "applications" subprocess.run(['update-desktop-database', str(apps_dir)], capture_output=True, timeout=10) diff --git a/jackify/backend/services/steam_restart_service.py b/jackify/backend/services/steam_restart_service.py index b90ed6d..cb5e84e 100644 --- a/jackify/backend/services/steam_restart_service.py +++ b/jackify/backend/services/steam_restart_service.py @@ -673,6 +673,7 @@ def robust_steam_restart(progress_callback: Optional[Callable[[str], None]] = No final_check = subprocess.run(['pgrep', '-f', 'steamwebhelper'], capture_output=True, timeout=10, env=start_env) if final_check.returncode == 0: report("Steam started successfully.") + report("[Jackify] Steam restart complete") logger.info(f"Steam confirmed running after {elapsed_wait}s wait.") return True else: diff --git a/jackify/backend/services/ttw_installer_service.py b/jackify/backend/services/ttw_installer_service.py new file mode 100644 index 0000000..5f72af2 --- /dev/null +++ b/jackify/backend/services/ttw_installer_service.py @@ -0,0 +1,62 @@ +"""Shared backend helpers for locating and installing TTW_Linux_Installer.""" + +from __future__ import annotations + +import logging +from pathlib import Path +from typing import Callable, Optional, Tuple + +from jackify.backend.handlers.config_handler import ConfigHandler +from jackify.backend.handlers.filesystem_handler import FileSystemHandler +from jackify.backend.handlers.ttw_installer_handler import TTWInstallerHandler + +logger = logging.getLogger(__name__) + + +def _build_handler() -> TTWInstallerHandler: + return TTWInstallerHandler( + steamdeck=False, + verbose=False, + filesystem_handler=FileSystemHandler(), + config_handler=ConfigHandler(), + ) + + +def get_ttw_installer_path() -> Optional[Path]: + """Return the resolved TTW_Linux_Installer executable path, if available.""" + handler = _build_handler() + path = handler.ttw_installer_executable_path + if path and path.exists(): + return path + return None + + +def ensure_ttw_installer_available( + progress_callback: Optional[Callable[[str], None]] = None, +) -> Tuple[Optional[Path], str]: + """ + Ensure TTW_Linux_Installer is installed and return its executable path. + + Returns: + (path, message) + """ + existing = get_ttw_installer_path() + if existing: + return existing, "TTW_Linux_Installer ready" + + if progress_callback: + progress_callback("TTW_Linux_Installer not found, installing...") + + handler = _build_handler() + success, message = handler.install_ttw_installer() + if not success: + logger.error("Failed to install TTW_Linux_Installer: %s", message) + return None, message + + path = handler.ttw_installer_executable_path + if path and path.exists(): + if progress_callback: + progress_callback("TTW_Linux_Installer installed successfully") + return path, message + + return None, "TTW_Linux_Installer install completed but executable was not found" diff --git a/jackify/backend/services/update_service.py b/jackify/backend/services/update_service.py index 09bc560..211fc8f 100644 --- a/jackify/backend/services/update_service.py +++ b/jackify/backend/services/update_service.py @@ -101,13 +101,15 @@ class UpdateService: break if download_url: - # Prefer Nexus CDN for Premium users when release embeds nexus_file_id - release_body = release_data.get('body', '') - nexus_url = self._try_nexus_download_url(release_body) + # Prefer Nexus CDN for Premium users if this version is available there + nexus_url = self._try_nexus_download_url(latest_version) update_source = "github" if nexus_url: download_url = nexus_url update_source = "nexus" + logger.debug(f"UPD-1001 update_source_selected source=nexus version={latest_version}") + else: + logger.debug(f"UPD-1001 update_source_selected source=github version={latest_version}") # Determine if this is a delta update is_delta = '.delta' in download_url or 'delta' in download_url.lower() @@ -152,54 +154,69 @@ class UpdateService: logger.error(f"Unexpected error checking for updates: {e}") return None - def _try_nexus_download_url(self, release_body: str) -> Optional[str]: - """ - If the user is Nexus Premium and the release body embeds nexus_file_id, - return a Nexus CDN download URL. Returns None on any failure. + _NEXUS_MOD_ID = 1427 - Release body format expected: - nexus_mod_id: 12345 - nexus_file_id: 67890 + def _try_nexus_download_url(self, target_version: str) -> Optional[str]: + """ + If the user is Nexus Premium, query the Nexus files list for the mod + and return a CDN download URL for the file matching target_version. + Returns None on any failure or if the version is not yet on Nexus. """ - import re try: - mod_match = re.search(r'nexus_mod_id:\s*(\d+)', release_body, re.IGNORECASE) - file_match = re.search(r'nexus_file_id:\s*(\d+)', release_body, re.IGNORECASE) - if not file_match: - return None - nexus_file_id = int(file_match.group(1)) - nexus_mod_id = int(mod_match.group(1)) if mod_match else None - from jackify.backend.services.nexus_auth_service import NexusAuthService auth_service = NexusAuthService() token = auth_service.get_auth_token() if not token: + logger.debug("UPD-1002 nexus_lookup_skipped reason=missing_auth_token") return None + auth_method = auth_service.get_auth_method() + is_oauth = auth_method == "oauth" from jackify.backend.services.nexus_premium_service import NexusPremiumService - is_premium, _ = NexusPremiumService().check_premium_status(token) + is_premium, _ = NexusPremiumService().check_premium_status(token, is_oauth=is_oauth) if not is_premium: - logger.debug("Nexus download skipped: user is not Premium") + logger.debug("UPD-1002 nexus_lookup_skipped reason=not_premium") return None - if nexus_mod_id is None: + auth_headers = {"Accept": "application/json"} + if is_oauth: + auth_headers["Authorization"] = f"Bearer {token}" + else: + auth_headers["apikey"] = token + + files_url = f"https://api.nexusmods.com/v1/games/site/mods/{self._NEXUS_MOD_ID}/files.json" + resp = requests.get(files_url, headers=auth_headers, timeout=8) + resp.raise_for_status() + files = resp.json().get("files", []) + + # Prefer MAIN category; accept any non-archived/removed file matching the version. + match = None + for f in files: + if f.get("version") != target_version: + continue + if f.get("category_name") == "MAIN": + match = f + break + if f.get("category_name") not in ("ARCHIVED", "REMOVED"): + match = match or f + + if match is None: + logger.debug(f"UPD-1002 nexus_lookup_skipped reason=version_not_on_nexus version={target_version}") return None - api_url = f"https://api.nexusmods.com/v1/games/site/mods/{nexus_mod_id}/files/{nexus_file_id}/download_link.json" - resp = requests.get( - api_url, - headers={"apikey": token, "Accept": "application/json"}, - timeout=8, - ) + nexus_file_id = match["file_id"] + dl_url = f"https://api.nexusmods.com/v1/games/site/mods/{self._NEXUS_MOD_ID}/files/{nexus_file_id}/download_link.json" + resp = requests.get(dl_url, headers=auth_headers, timeout=8) resp.raise_for_status() links = resp.json() if isinstance(links, list) and links: cdn_url = links[0].get("URI") if cdn_url: - logger.debug(f"Using Nexus CDN URL for update") + logger.debug(f"UPD-1003 nexus_lookup_success file_id={nexus_file_id} version={target_version}") return cdn_url + logger.debug("UPD-1002 nexus_lookup_skipped reason=empty_download_links") except Exception as e: - logger.debug(f"Nexus download URL lookup failed: {e}") + logger.debug(f"UPD-1004 nexus_lookup_failed error={e}") return None def _is_newer_version(self, version: str) -> bool: @@ -494,4 +511,4 @@ rm -f "{helper_script}" except Exception as e: logger.error(f"Failed to create update helper script: {e}") - return None \ No newline at end of file + return None diff --git a/jackify/backend/services/vnv_post_install_service.py b/jackify/backend/services/vnv_post_install_service.py index fdc381b..1066abf 100644 --- a/jackify/backend/services/vnv_post_install_service.py +++ b/jackify/backend/services/vnv_post_install_service.py @@ -13,6 +13,7 @@ Uses native Linux tools (no Wine required) by downloading from Nexus with OAuth. import logging import os +import json import shutil import subprocess import stat @@ -83,6 +84,110 @@ class VNVPostInstallService: self.download_service = NexusDownloadService(auth_token) return True + def _ensure_download_service(self, progress_callback: Optional[Callable[[str], None]] = None) -> bool: + if self.download_service is not None: + return True + return self._ensure_auth(progress_callback) + + def _find_cached_4gb_patcher(self) -> Optional[Path]: + for path in self.cache_dir.iterdir(): + if path.is_file() and path.suffix.lower() == ".zip" and "4gb" in path.name.lower(): + return path + for path in self.cache_dir.iterdir(): + if path.is_dir() and path.name.lower().endswith("_extracted") and "4gb" in path.name.lower(): + for child in path.iterdir(): + if child.is_file(): + return child + return None + + def _find_cached_bsa_mpi(self) -> Optional[Path]: + for path in self.cache_dir.iterdir(): + if path.is_file() and path.suffix.lower() == ".mpi" and "bsa" in path.name.lower(): + return path + for path in self.cache_dir.iterdir(): + if path.is_dir() and path.name.lower().endswith("_extracted") and "bsa" in path.name.lower(): + for child in path.rglob("*.mpi"): + if child.is_file(): + return child + return None + + def _find_cached_bsa_package(self) -> Optional[Path]: + preferred = [] + fallback = [] + for path in self.cache_dir.iterdir(): + if not path.is_file(): + continue + lower = path.name.lower() + if "bsa" not in lower or path.suffix.lower() not in {".zip", ".7z"}: + continue + if path.suffix.lower() == ".zip": + preferred.append(path) + else: + fallback.append(path) + candidates = sorted(preferred) or sorted(fallback) + return candidates[0] if candidates else None + + def _extract_bsa_package(self, archive_path: Path) -> tuple[bool, Optional[Path], str]: + extract_dir = self.cache_dir / f"{archive_path.stem}_extracted" + mpi_path = next((p for p in extract_dir.rglob("*.mpi") if p.is_file()), None) if extract_dir.exists() else None + if mpi_path: + return True, mpi_path, f"Using extracted BSA package from {archive_path.name}" + + extract_dir.mkdir(parents=True, exist_ok=True) + try: + suffix = archive_path.suffix.lower() + if suffix == ".zip": + with zipfile.ZipFile(archive_path, 'r') as zip_ref: + zip_ref.extractall(extract_dir) + elif suffix == ".7z": + result = subprocess.run( + ["7z", "x", "-y", f"-o{extract_dir}", str(archive_path)], + capture_output=True, + text=True, + check=False, + ) + if result.returncode != 0: + return False, None, (result.stderr or result.stdout or "7z extraction failed").strip() + else: + return False, None, f"Unsupported BSA package format: {archive_path.name}" + except Exception as e: + return False, None, str(e) + + mpi_path = next((p for p in extract_dir.rglob("*.mpi") if p.is_file()), None) + if not mpi_path: + return False, None, f"No .mpi file found in BSA package: {archive_path.name}" + return True, mpi_path, f"Extracted BSA package {archive_path.name}" + + @staticmethod + def _select_manual_download_file(files: list[dict], mod_id: int) -> Optional[dict]: + def _active(entries: list[dict]) -> list[dict]: + return [f for f in entries if f.get("category_name") not in ("ARCHIVED", "REMOVED")] + + active_files = _active(files) + if mod_id == VNVPostInstallService.LINUX_4GB_PATCHER_MOD_ID: + proton_files = [ + f for f in active_files + if "proton" in f.get("file_name", "").lower() and f.get("file_name", "").lower().endswith(".zip") + ] + if proton_files: + proton_files.sort(key=lambda f: f.get("uploaded_timestamp", 0), reverse=True) + return proton_files[0] + if mod_id == VNVPostInstallService.FNV_BSA_DECOMPRESSOR_MOD_ID: + zip_files = [f for f in active_files if f.get("file_name", "").lower().endswith(".zip")] + if zip_files: + zip_files.sort(key=lambda f: f.get("uploaded_timestamp", 0), reverse=True) + return zip_files[0] + + main_files = [f for f in active_files if f.get("category_name") == "MAIN"] + if main_files: + main_files.sort(key=lambda f: f.get("uploaded_timestamp", 0), reverse=True) + return main_files[0] + + if active_files: + active_files.sort(key=lambda f: f.get("uploaded_timestamp", 0), reverse=True) + return active_files[0] + return None + def should_run_automation(self, modlist_name: str) -> bool: """ Check if this modlist should trigger VNV automation. @@ -108,11 +213,60 @@ class VNVPostInstallService: "1. Copy root mods to game directory\n" "2. Download and run Linux 4GB patcher\n" "3. Download and run BSA decompressor (reduces loading times)\n\n" - "Premium users: Downloads happen automatically\n" - "Non-Premium users: You'll be prompted to download files manually\n\n" + "Jackify will download the required tools automatically where possible.\n" + "If you are not a Nexus Premium member, you will be prompted to\n" + "manually download any tools that cannot be fetched automatically.\n\n" "Would you like Jackify to automate these steps?" ) + def get_manual_download_items(self, include_bsa: bool = False) -> list: + """ + Query Nexus for the current MAIN file of each required VNV tool and return + a list of DownloadItem-compatible event dicts for use with ManualDownloadManager. + Works with any Nexus auth (not Premium-only). + Returns an empty list if auth is unavailable or queries fail. + """ + import requests as _requests + token = self.auth_service.get_auth_token() + if not token: + return [] + auth_method = self.auth_service.get_auth_method() + headers = {"Accept": "application/json"} + if auth_method == "oauth": + headers["Authorization"] = f"Bearer {token}" + else: + headers["apikey"] = token + + tools = [(self.LINUX_4GB_PATCHER_MOD_ID, "4GB Patcher")] + if include_bsa: + tools.append((self.FNV_BSA_DECOMPRESSOR_MOD_ID, "BSA Decompressor")) + items = [] + for mod_id, label in tools: + try: + resp = _requests.get( + f"https://api.nexusmods.com/v1/games/newvegas/mods/{mod_id}/files.json", + headers=headers, timeout=8, + ) + resp.raise_for_status() + files = resp.json().get("files", []) + match = self._select_manual_download_file(files, mod_id) + if match is None: + logger.warning(f"VNV tool lookup: no suitable file found for mod {mod_id} ({label})") + continue + file_id = match["file_id"] + items.append({ + "file_name": match["file_name"], + "mod_name": label, + "nexus_url": f"https://www.nexusmods.com/newvegas/mods/{mod_id}?tab=files&file_id={file_id}", + "expected_hash": "", + "expected_size": match.get("size_kb", 0) * 1024, + "mod_id": mod_id, + "file_id": file_id, + }) + except Exception as e: + logger.warning(f"VNV tool lookup failed for mod {mod_id} ({label}): {e}") + return items + def check_already_completed(self) -> dict: """ Check which VNV automation steps have already been completed. @@ -158,11 +312,6 @@ class VNVPostInstallService: logger.info(msg) try: - # Ensure authentication - update_progress("Checking Nexus authentication...") - if not self._ensure_auth(progress_callback): - return False, "Nexus authentication required. Please authenticate in Settings." - # Step 1: Copy root mods update_progress("Step 1/3: Copying root mods to game directory...") success, msg = self.copy_root_mods() @@ -253,25 +402,15 @@ class VNVPostInstallService: return True, "Game already patched (backup exists)" # Check cache first - look for extracted executable or zip - patcher_path = None - cached_extracted = list(self.cache_dir.glob("*4gb*_extracted/*")) - if cached_extracted: - # Use already extracted executable - for f in cached_extracted: - if f.is_file(): - patcher_path = f - logger.info(f"Using cached extracted 4GB patcher: {patcher_path}") - break - - if not patcher_path: - cached_files = list(self.cache_dir.glob("*4gb*.zip")) - if cached_files: - patcher_path = cached_files[0] - logger.info(f"Using cached 4GB patcher zip: {patcher_path}") + patcher_path = self._find_cached_4gb_patcher() + if patcher_path: + logger.info(f"Using cached 4GB patcher: {patcher_path}") if not patcher_path: # Try to download from Nexus # Linux version is named "FNV4GB for Proton", not "linux" + if not self._ensure_download_service(progress_callback): + return False, "Nexus authentication required to download the 4GB patcher." success, patcher_path, msg = self.download_service.download_latest_file( self.GAME_DOMAIN, self.LINUX_4GB_PATCHER_MOD_ID, @@ -394,60 +533,58 @@ class VNVPostInstallService: return True, "BSA decompression already completed" if not self.ttw_installer_path or not self.ttw_installer_path.exists(): - logger.warning("TTW_Linux_Installer not found, skipping BSA decompression") - return True, "BSA decompression skipped (TTW_Linux_Installer not available)" + from .ttw_installer_service import ensure_ttw_installer_available - # Check cache first - cached_files = list(self.cache_dir.glob("*BSA*.mpi")) - if cached_files: - mpi_path = cached_files[0] + self.ttw_installer_path, message = ensure_ttw_installer_available(progress_callback) + if not self.ttw_installer_path: + return False, f"TTW_Linux_Installer is required for BSA decompression: {message}" + + mpi_path = self._find_cached_bsa_mpi() + if mpi_path: logger.info(f"Using cached BSA Decompressor MPI: {mpi_path}") else: - # Also check for exact filename match (handles spaces in filename) - exact_path = self.cache_dir / "FNV BSA Decompressor.mpi" - if exact_path.exists(): - mpi_path = exact_path - logger.info(f"Using cached BSA Decompressor MPI: {mpi_path}") - else: - # Try to download from Nexus - # Look for files with .mpi extension (TTW installer format) - success, mpi_path, msg = self.download_service.download_latest_file( + package_path = self._find_cached_bsa_package() + if not package_path: + if not self._ensure_download_service(progress_callback): + return False, "Nexus authentication required to download the BSA Decompressor." + success, package_path, msg = self.download_service.download_latest_file( self.GAME_DOMAIN, self.FNV_BSA_DECOMPRESSOR_MOD_ID, self.cache_dir, - file_name_filter=".mpi", + file_name_filter=".zip", progress_callback=progress_callback ) - if not success: - # Download failed - offer manual download logger.warning(f"Automatic download failed: {msg}") - if not manual_file_callback: - return False, f"Failed to download BSA Decompressor MPI: {msg}\n\nPlease download manually from: https://www.nexusmods.com/newvegas/mods/65854" + return False, f"Failed to download BSA Decompressor package: {msg}\n\nPlease download manually from: https://www.nexusmods.com/newvegas/mods/65854" instructions = ( "Automatic download failed (requires Nexus Premium).\n\n" - "Please download the FNV BSA Decompressor manually:\n" + "Please download the FNV BSA Decompressor package manually:\n" "1. Visit: https://www.nexusmods.com/newvegas/mods/65854\n" - "2. Download the .mpi file\n" - "3. Select the downloaded file below" + "2. Download the zip package\n" + "3. Select the downloaded archive below" ) + selected_path = manual_file_callback("BSA Decompressor Required", instructions) + if not selected_path or not selected_path.exists(): + return False, "BSA Decompressor package not provided" + if selected_path.suffix.lower() not in {'.zip', '.7z', '.mpi'}: + return False, f"Selected file is not a supported BSA package: {selected_path}" + cached_path = self.cache_dir / selected_path.name + shutil.copy2(selected_path, cached_path) + package_path = cached_path + logger.info(f"Using manually selected BSA package: {package_path}") - mpi_path = manual_file_callback("BSA Decompressor Required", instructions) - - if not mpi_path or not mpi_path.exists(): - return False, "BSA Decompressor MPI file not provided" - - # Validate it's an MPI file - if not mpi_path.suffix.lower() == '.mpi': - return False, f"Selected file is not an MPI file: {mpi_path}" - - # Copy to cache for future use - cached_path = self.cache_dir / mpi_path.name - shutil.copy2(mpi_path, cached_path) - mpi_path = cached_path - logger.info(f"Using manually selected BSA Decompressor MPI: {mpi_path}") + if package_path.suffix.lower() == ".mpi": + mpi_path = package_path + else: + if progress_callback: + progress_callback("Preparing BSA decompressor package...") + success, mpi_path, msg = self._extract_bsa_package(package_path) + if not success or not mpi_path: + return False, f"Failed to prepare BSA Decompressor package: {msg}" + logger.info(msg) # Create temp output directory with tempfile.TemporaryDirectory() as temp_output: @@ -455,7 +592,6 @@ class VNVPostInstallService: # Create config file for TTW_Linux_Installer (handles spaces in paths better) config_file = self.ttw_installer_path.parent / "ttw-config.json" - import json config_data = { "FalloutNVRoot": str(self.game_root), "MpiPackagePath": str(mpi_path), @@ -467,6 +603,7 @@ class VNVPostInstallService: # Run via TTW_Linux_Installer if progress_callback: + progress_callback("Ensuring TTW_Linux_Installer is available...") progress_callback("Running BSA decompressor...") cmd = [ diff --git a/jackify/backend/services/wabbajack_installer_service.py b/jackify/backend/services/wabbajack_installer_service.py index 2e02357..4a3190b 100644 --- a/jackify/backend/services/wabbajack_installer_service.py +++ b/jackify/backend/services/wabbajack_installer_service.py @@ -63,6 +63,7 @@ class WabbajackInstallerService: install_folder: Path, shortcut_name: str = "Wabbajack", enable_gog: bool = True, + existing_appid: Optional[int] = None, progress_callback: Optional[Callable[[str, int], None]] = None, log_callback: Optional[Callable[[str], None]] = None ) -> Tuple[bool, Optional[int], Optional[str], Optional[int], Optional[str], Optional[str]]: @@ -128,34 +129,6 @@ class WabbajackInstallerService: self.handler.create_dotnet_cache(install_folder) update_progress(".NET cache created", 3, 20) - # Step 4: Stop Steam briefly (required to safely modify shortcuts.vdf) - # We'll do a full restart after creating the shortcut - update_progress("Stopping Steam to modify shortcuts...", 4, 25) - try: - shutdown_env = _get_clean_subprocess_env() - - if _is_steam_deck: - subprocess.run(['systemctl', '--user', 'stop', 'app-steam@autostart.service'], - timeout=15, check=False, capture_output=True, env=shutdown_env) - elif _is_flatpak: - subprocess.run(['flatpak', 'kill', 'com.valvesoftware.Steam'], - timeout=15, check=False, capture_output=True, env=shutdown_env) - - subprocess.run(['pkill', 'steam'], timeout=15, check=False, capture_output=True, env=shutdown_env) - time.sleep(2) - - check_result = subprocess.run(['pgrep', '-f', 'steamwebhelper'], capture_output=True, timeout=10, env=shutdown_env) - if check_result.returncode == 0: - subprocess.run(['pkill', '-9', 'steam'], timeout=15, check=False, capture_output=True, env=shutdown_env) - time.sleep(2) - - update_progress("Steam stopped", 4, 25) - except Exception as e: - update_progress(f"Warning: Steam shutdown had issues: {e}. Proceeding anyway...", 4, 25) - - # Step 5: Create Steam shortcut using NativeSteamService - update_progress("Adding Wabbajack to Steam shortcuts...", 5, 30) - # Generate launch options with STEAM_COMPAT_MOUNTS launch_options = "" try: @@ -170,27 +143,58 @@ class WabbajackInstallerService: except Exception as e: update_progress(f"Could not generate STEAM_COMPAT_MOUNTS (non-critical): {e}", 5, 30) - success, app_id = self.steam_service.create_shortcut_with_proton( - app_name=shortcut_name, - exe_path=str(wabbajack_exe), - start_dir=str(wabbajack_exe.parent), - launch_options=launch_options, - tags=["Jackify"], - proton_version=proton_compat_name - ) - if not success or app_id is None: - return False, None, None, None, None, "Failed to create Steam shortcut" - update_progress(f"Created Steam shortcut with AppID: {app_id}", 5, 30) + if existing_appid is None: + # Step 4: Stop Steam briefly (required to safely modify shortcuts.vdf) + # We'll do a full restart after creating the shortcut + update_progress("Stopping Steam to modify shortcuts...", 4, 25) + try: + shutdown_env = _get_clean_subprocess_env() - # Step 5b: Restart Steam (same pattern as modlist workflows) - update_progress("Restarting Steam...", 5, 35) - def restart_callback(msg): - update_progress(msg, 5, 35) + if _is_steam_deck: + subprocess.run(['systemctl', '--user', 'stop', 'app-steam@autostart.service'], + timeout=15, check=False, capture_output=True, env=shutdown_env) + elif _is_flatpak: + subprocess.run(['flatpak', 'kill', 'com.valvesoftware.Steam'], + timeout=15, check=False, capture_output=True, env=shutdown_env) - if not robust_steam_restart(progress_callback=restart_callback): - update_progress("Warning: Steam restart had issues, continuing anyway...", 5, 35) + subprocess.run(['pkill', 'steam'], timeout=15, check=False, capture_output=True, env=shutdown_env) + time.sleep(2) + + check_result = subprocess.run(['pgrep', '-f', 'steamwebhelper'], capture_output=True, timeout=10, env=shutdown_env) + if check_result.returncode == 0: + subprocess.run(['pkill', '-9', 'steam'], timeout=15, check=False, capture_output=True, env=shutdown_env) + time.sleep(2) + + update_progress("Steam stopped", 4, 25) + except Exception as e: + update_progress(f"Warning: Steam shutdown had issues: {e}. Proceeding anyway...", 4, 25) + + # Step 5: Create Steam shortcut using NativeSteamService + update_progress("Adding Wabbajack to Steam shortcuts...", 5, 30) + success, app_id = self.steam_service.create_shortcut_with_proton( + app_name=shortcut_name, + exe_path=str(wabbajack_exe), + start_dir=str(wabbajack_exe.parent), + launch_options=launch_options, + tags=["Jackify"], + proton_version=proton_compat_name + ) + if not success or app_id is None: + return False, None, None, None, None, "Failed to create Steam shortcut" + update_progress(f"Created Steam shortcut with AppID: {app_id}", 5, 30) + + # Step 5b: Restart Steam (same pattern as modlist workflows) + update_progress("Restarting Steam...", 5, 35) + def restart_callback(msg): + update_progress(msg, 5, 35) + + if not robust_steam_restart(progress_callback=restart_callback): + update_progress("Warning: Steam restart had issues, continuing anyway...", 5, 35) + else: + update_progress("Steam restarted successfully", 5, 40) else: - update_progress("Steam restarted successfully", 5, 40) + app_id = int(existing_appid) + update_progress(f"Reusing existing Steam shortcut with AppID: {app_id}", 5, 30) # Step 6: Initialize Wine prefix (using same method as modlist workflows) update_progress("Creating Proton prefix...", 6, 45) @@ -277,4 +281,3 @@ class WabbajackInstallerService: if log_callback: log_callback(f"ERROR: {error_msg}") return False, None, None, None, None, error_msg - diff --git a/jackify/backend/utils/cc_content_detector.py b/jackify/backend/utils/cc_content_detector.py new file mode 100644 index 0000000..2f2ce6b --- /dev/null +++ b/jackify/backend/utils/cc_content_detector.py @@ -0,0 +1,34 @@ +""" +Detects Creation Club / Anniversary Edition content missing errors in engine output. +""" + +import re +from typing import Optional + +# Matches CC content file names: ccXXXsse001-name.bsa/esm/esl/esp, ccXXXfo4001-name.ba2, etc. +# No leading \b — filenames often appear with a Data_ prefix (Data_ccbgssse019-...) +# where _ is a word char and would prevent \b from matching. +_CC_FILE_RE = re.compile( + r'cc[a-z]{2,8}\d{3,4}[-\w]*\.(?:bsa|esm|esl|esp|ba2)', + re.IGNORECASE +) + +_ERROR_WORDS = frozenset(( + 'missing', 'required', 'failed', 'unable', 'cannot', 'error', 'not found', +)) + + +def is_cc_content_error(line: str) -> bool: + """Return True if line indicates a missing CC/AE content file in an error context.""" + if not line: + return False + normalized = line.strip().lower() + if not _CC_FILE_RE.search(normalized): + return False + return any(w in normalized for w in _ERROR_WORDS) + + +def extract_cc_filename(line: str) -> Optional[str]: + """Return the CC filename from a line, or None if not found.""" + m = _CC_FILE_RE.search(line) + return m.group(0) if m else None diff --git a/jackify/backend/utils/engine_error_parser.py b/jackify/backend/utils/engine_error_parser.py index 53c4d81..ab1613b 100644 --- a/jackify/backend/utils/engine_error_parser.py +++ b/jackify/backend/utils/engine_error_parser.py @@ -1,8 +1,10 @@ import json +import re from typing import Optional from jackify.shared.errors import ( JackifyError, InstallError, OAuthError, oauth_expired, wabbajack_install_failed, format_technical_context, + game_not_found_for_modlist, ) @@ -12,7 +14,29 @@ def _ctx_detail(ctx: dict) -> Optional[str]: return format_technical_context(context=ctx) +def _engine_error(msg: str, ctx: dict) -> InstallError: + """Map generic engine_error payloads to user-visible, actionable InstallError variants.""" + text = (msg or "").strip() + match = re.search(r"can't find game\s+([A-Za-z0-9_:-]+)", text, flags=re.IGNORECASE) + if match: + game_name = match.group(1) + return game_not_found_for_modlist(game_name, detail=text) + + return InstallError( + "Install Engine Error", + text or "An install engine error occurred.", + suggestion="Review the error message and retry after correcting the reported issue.", + solutions=[ + "Check the exact error message shown above and fix the prerequisite it mentions.", + "Retry the install after restarting Steam.", + "If this persists, check Modlist_Install_workflow.log for the same error text.", + ], + technical=_ctx_detail(ctx), + ) + + _TYPE_MAP = { + "engine_error": _engine_error, "auth_failed": lambda msg, ctx: oauth_expired(), "premium_required": lambda msg, ctx: InstallError( "Nexus Premium Required", diff --git a/jackify/engine/Wabbajack.CLI.Builder.dll b/jackify/engine/Wabbajack.CLI.Builder.dll index 45946489e075b0cdaffceb7ca05c72861b22380c..3663399542f06a0f790d014a4de47b52ca4ebd7f 100644 GIT binary patch delta 8983 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 E0hQxx&j0`b delta 6900 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 LuR0+%mze(pJ%HIR diff --git a/jackify/engine/Wabbajack.Common.dll b/jackify/engine/Wabbajack.Common.dll index 6bd9eb94a61de7a994a7a4262c8b10642564ca40..1153b5070d391463055d2d45646a001bfa0e8284 100644 GIT binary patch delta 423 zcmZoT!qaesXF>-HvobJDfBlS6SsTcJ14ms56Im>N0bFdl<#R?= zu+GHijBmk=z!wma?UgSW_g@sa!=o>J*VWpyY1ce!r^CK(+dXzMi8KW)6FfbT1 zG5{Gsj&@R_Wm;;gX_`fHVp?LNMOsR-g{4WVfk~Q$iK&TEnx%!IfniE=Y8nHBIU|G7 z^n~9`n%j^4VhR&r*}6{m&vXTD<^X{kdgo7k*q!aNB1mpp-r zg4B!c`cS2F=iD!g3oh5Tv+*!D3Gl;w2&K1AmthWMWHn{bV=&nMR+ibCF&xZh&}K+t zNMx{NNMlF^LQ^1Z!H^6TNdw|UAUlmA1<1DmsxSe{8vsd=3KO7869yw7+Y-n#1d1Di e)ul3|O|O?{HspjD4zz6gL}_M^?TiY{{}}<#*>AG| delta 423 zcmZoT!qaesXF>;ypuy6X#-6P`j4{ucSr|B`zkbH3tPNx^z<{GJgoP{?zW^>a-SRo3 zDp+UYbH=w|M&JvG$o9$?jQcMNC@cK`m6iX-x$Akw5tZJowCx_dn8Y0HP4q1E3>X-U z85#5p^ne^~V{?Nf!z5Ef%apVQwx(61_pCR z2BYZ-znL_*AN$1=Cctvv^y%yA3f#;A0+tQyRT}tw+*W9P)G;{!@67ZDZss%vs3_YD zs3=f<_C$sU-!5pb@!n_?zj8Yp4|9_MKMUAX91Ku;`*a!RKt@&*20aFg?Qdn7tr^3? zYzA!xV+L~u1BN69Lm)NTR3e5i*0ovVZU;qFB diff --git a/jackify/engine/Wabbajack.Compiler.dll b/jackify/engine/Wabbajack.Compiler.dll index 6931ba8af3f49a206f77178338fde0e21806a225..5d1be7c598ca418d16726d307246f639456c3bd1 100644 GIT binary patch delta 560 zcmZp8!rAbIb3zA;h0nW>jXhg?7$drvSs56nKks5xP6smJV74uUi7fWP22)Jb0aI+R zJ*L=aNx0bb!fr-Yu&pzJ3~gi^p1Q$RK^Q=?)(9>({be_!r8u&N?*a%(%O1wLU}JCg zFm{3%+f#cP^XmkxchuV5na}04EOfKF)j7MX+n;bVef6|6)icmDU|=w2WB@XN9POk; z%e2%~(=?0Z#I(dji?ozv3rmw!1Culh6H^nTG)oIZ1H+W$)HDVL7Dk5YjbTj2+r7e= zqJ&wDTKSGmUy;caAh1=uYp21VcE{PXC2l3M+3uXqk;Rmy02RF`4HX5dcVo0HbPaob zBf^pUa_#m>Sxny;`Ca zDUh~cNCt|e0dXRboyL#?-OkX|iO!O@D3>X-U85#5p^ne^~ zV{?Nf!z5Ef%apVQwx(61_l;JhUtxAOvc;2 z!kD6jS<*G+-cMhV$rK=9W_wRDAg;@yQ_y!;j!43d=^R;1Sqe~51(+yMeZJd@UG=SB zR?M!n`MY=fq%5XyjQlKMuW&Fx>Fv{Vm?9ZjO&IhTEVh5mWpZZ>2eTQp8H^ds84MVb z7z}~f6i8Y!q%fobp%IX60Hn=;DvTIRfH)N>mj+Y?VkZLCCIjWn7%afDX+XXOP$p&i aQ4Y=55)Bno0 z0+;%ZpKtQN=RD^uXU7!vY0tK2*f44hld#ZH}^ekP4EKw$r8hB`Z6 zMfVN+qBb}j3T>ILlg?P702N&h69uaGENaw#aPGSM#G6yY+O{7_XFS5h5AzO`-ab8x zv5=9~ltGWdU^{OPV>V+rn9ZQgki?M4V9AiikP3vRK-z*K87Ptl#EC$58bb<@Zvj+c z0+crZk{}f(K$Rv8MnJYDkY@-KHw3FoWk{QTK9|vu6Jj{XGQm8?4~!EHIJPquF#cx* E0ADy}>i_@% delta 404 zcmZp8!rJhJbwUSAM8*4q8+#1?F|#mmOy2*`+#X0Xz`#;%2n$(kvI?eHt_ECev*~|j zTY=Rl__qs&@AleurTpj)Q5}`-88(dCik2pN7J3E@491KMdIowxj<&J6L6TvTsi9>` zTAGoeL7JJdk%>{NWm=MnQKD(GnVE%=Nt&sJNy_#!v5bE>SQ7L8PM>}zjWIyL{q(A0 z!I*f*m6~g~4)g~vp01P5SfKzF4TOn;)H|vKG%aLZvB;WTTzLDDbjBl0{48MCaxg&Y z?bEXu3mI8W81xt{w)5sNW;2F^*$mnY#th~R1`J6IhCplzBrO?I7}9{y2*@@7(q=#v zMhqrEoC=gn1F8bC6M<@zfpTUH7GT*lAm0KglQR8$E~6nQ#Be$8gOi9 JEMWZ42mlzvUm*Yh diff --git a/jackify/engine/Wabbajack.Compression.Zip.dll b/jackify/engine/Wabbajack.Compression.Zip.dll index cf5f03d445da93cba220845b28f2fe0170958d27..55ed1bfe8636478f9fb02e63ee0ad1d252b85a74 100644 GIT binary patch delta 333 zcmZpe!q_l{aY6@+bCdjyjXefh%&ZIyllN;WgXqmqwchgzTsy-d{FeGA%XLG|eJ8F)cCCA}uA^!qOzwz$DGW#MHzn z&CHue~3F|#mmOx~}h45Bwb)q2k>@J~CL&GNUJ-^7iFZhcph zPTBm#VlAtsiJpa?0Rw|EBZHoS9+0DLY;KTbm}F{bnUa=fWN46PW^80)lxmrlWMY(P znrvofVPukKYGIPH`IOx$MwYXgs~1n6;SeA&J4ND0@BRheJ-;V;7#$1toczO~LIEn6 z%>xw#sXpV(xaiQCH7=_-AAX%Y!SM$_3j+vnFhJ?e)13+#Sxp%97%VpPx@0qkgP9E4 z48{!R3IM7 jG$7vsD3dbzysIH6#B4nVgUy0&9~dVZaBODu_|FIcY@c6Q diff --git a/jackify/engine/Wabbajack.Configuration.dll b/jackify/engine/Wabbajack.Configuration.dll index 005de8d50e72c2c161f87a4d20621511c646adfe..dd09668f9f82ade110463af402c8b56f1ba3e5ab 100644 GIT binary patch delta 311 zcmZorXi%8Y!BVq>bK}My8)jx!28PY2nHiY`IC^*gn!PH^VUCr~DN9!Vy_4^-?sYcR zGte_&U@&H605X6a?W9D@wA577G>hcKw8TVWS1p3l$cU_j6;xm8K#Hby%G6|Dy*h>_kf<5t2L6GVtKCR0=|0(ns zFRG8(e1Uxj6F<;_U;w2zPv^>HWHn{bV=&mv$`j8R4rVfFGbAx2GFURCF{A>aDUh~c zNCt|e0dXRboyL#?Y Gp9uh63RSHD delta 311 zcmZorXi%8Y!BSxK=*7k!8)jw}29C|AnHiY`OeWpR;^lVrYdR^zq{MAEXYw7^z0M|j z7J3E@491KMdIowxj<&J6L6TvTsi9>`TAGoeL7JJdk%>{NWm=MnQKD(GnVE%=Nt&sJ zNeTlPNc;M4oP38ZK;ZqwWw#rysrXK)J|*zSgZ0d08}(^b diff --git a/jackify/engine/Wabbajack.DTOs.dll b/jackify/engine/Wabbajack.DTOs.dll index 9c5581fc75e5dd44afc32f905c2d41edf3c9aff3..7827d81ef67536a7891951b40bc266b2c420babc 100644 GIT binary patch delta 17207 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 I1EZt=8!0W=m;e9( delta 17159 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 T`svo62Omq!qhE;HUlRRaEUCCx diff --git a/jackify/engine/Wabbajack.Downloaders.Bethesda.dll b/jackify/engine/Wabbajack.Downloaders.Bethesda.dll index e926c84e9eaf632243b615365887d2969454b395..06a343f197778d862143a8a956bdd49bc29a6fd4 100644 GIT binary patch delta 450 zcmZpez}PT>aY6^n@xr;wHuh{%U}j}tnCz&iY!9U2K$jcBL>7C>g(=p{3m2QbSkY1( zS$!fSLhiGoCDeSB|4rVfFGbAx2GFURCF{A>a zDUh~cNCt|e0dXRboyL#?aY6^nBK{RyHuh{%U}j<9nCz&iY!9RvU_h4}!a^2%%7rP`%L^Bqyjamv z99exLBSP-8q9xcKeI;KMHSZB>HcwV+6c7mdFs1#m-bUB0pJqRZsTa}OtYSQm-O@zQ zLeGGK!I+Uj&p;2z(Ka?WNHR<^HMC4gOEWSwNHa4wGBHZEOiMB`N;FM2GqW%HZv|;J#F^wCjzX)2hiewoM99!S7F@fq1YO%VGHwi*xLM9Fi;15F>>&;YD7|^QLnkAv34%{gc(z81Hvx2d2Ku>>Lx4Nc%rx=h!aH~%z1&T2P`&~1ykKkHcT-gEb6ui zp{Pp$^Avbd1i>6VEShbwSW<>X-9AKMZuYcgmJ)ayp~tCcAmKiJonh)l&0js6W5UIF zEKT(c^b8mnj2Rh#3?N54DbX@5HPtlDA~`WFG0`F|CE3E#B-Owq&BDag#3;?u!qC7l zB{?;1^Xg*ma63B2Z5*aKR(il>K z&=g2pFeC#-(ttP-$WCKO0rD+?DolX#20#*|!UU+&guw{NwgmDFf#QZ>b*T(#ljZXa TIU#0)ta8j)!MK^R;6Eb(Y9ncx delta 712 zcmZp8!r1VHaY6@+kM{578+*1`F|#mmOm?H&*QWx-T;lMPc$2#dOH zLMZAIz&r(B6hSaY4~u3SES8jEQMV5fn43LqnWY4{@?<~Gy1mk2=L_4t8&}JDZH@^S zYr&N}S2tx%pZ8A|`%_<2e|h^ycYV6B$`e81xt{HtXihW()^28MGOU8O#|B z7?Kzaf!GvCS~8?CqyeE3kZk~@&44P57)*dT6)2YmR0U!u0@Wr1<;)l?z_MvTz6DSw aWwLypAt%IaJqCl#ju|T$H!~LeX9NK8Eo;yK diff --git a/jackify/engine/Wabbajack.Downloaders.GameFile.dll b/jackify/engine/Wabbajack.Downloaders.GameFile.dll index a54cf0f28bf0916fbf3f065327a9ada71fbc8060..8b76c641fd998a7db027a3cd8ed1b3504656a35e 100644 GIT binary patch delta 429 zcmZo@U~Fh$oY29t>FeQD8+$HEFtai+Oiq+EPY2R)P|povB8$ah5xc^Osm=_G*b*MN zy2<|~--7Ji{9bCKkih8|yO(`jY3sTl0(Hl2^_Gkl?fAl34nIx_2L8CB$2U9;I-XU4_E z&jNNY2LqJeJl&#`k=2AjkHKQIs#QN@IGD+x&0x%6&S1ch#9#=-ra;n?A%!6g2#tVj y10Zb%RAIzm0>r66xip|E5IYg5HW?^q#$W-KO#|{RfHEnQxor%g);QYiX9fT^TUxvT diff --git a/jackify/engine/Wabbajack.Downloaders.GoogleDrive.dll b/jackify/engine/Wabbajack.Downloaders.GoogleDrive.dll index 95d808bff8d51bb635b4a782b40b26c0454817eb..362edb36316d10557bcd18b0c153abeb07ef06d0 100644 GIT binary patch delta 435 zcmZqZVQlDOoY28?k9GNkjXi8~%&ZIylc&m=ivwvmsO5q%L2{4fEWv8zeXEyIJGGn(i)icmD zU|=w2WB@XN9POk;%e2%~(=?0Z#I(dji?ozv3rmw!1Culh6H^nTG)oIZ1H+W$)U?g& zRx=q{#Q1y;Pd;KDAn>y5Z1a+rsorh!GXi2REZa6&#im06DtOurDhN`2H^#RpF)H}! zRNI3oo6p$nVdRJT5=w8LZa0yU)s#Vx!CA&DW8!IB}3Ar%NsfwTof zGEgK9h!cVAG=>x)-vX$@1SoF+Bta@nfGSNGjDT!QAkPpeZU|PF%8)i$-pP;?Vm8Ps OL6;Sbn;BjHGXem~gJwcZW#^&ep2ZaRELl<2M{2A`Na7xu|-hxSWn|BzQv0Iwx zS?C!sFc>p3=o#n%IoihN21$lVriPX&X=z4=25Dx-MkYq7mT5^QMv11$W@Z*fCTXS? zCMlcMt!6T^r2J9)IQfWmfPliUw4F;IzxAJ<;r?EF`GhHxRctyGpn~}@L6B-DCoZdk zr%M+1<~&c|e8y%EBR>n+6C4asdh>Ln)4y zi{#c6%w`hXJb9YwCw`cBq4ehIW|fSrrVM%v2Af4K${EAKOa^U+B!)x=ONKOtR3J13 z(iRNKK#?>cP6V>k7*c?I3!n-Upu7Q)1gS6qsx)CR0=eJN-K*4X$Bbh#tC78m=`xP|0x5-h+^d<&JU4W0HrQ+Un`83}?PF~ACVCcn1`G_w zj0}1PdO(i0vAIE#VUnq#WlCC_k)c7FnX!?HQL1HHl8I5GX|kD_g^@{`sf9@j1A`qS zgXQFn##)=jjT;$Rg45hvCeJYm5b$$2e<@I-!DsS~%dJ&r+_IDZm{ci11$Bg>fDc=XBikAuXLzmun#e&S~V`-X!7N^hQSR>{a}!l1`sv02ojoG~2CWYA_XW-woFK?7PNW7xS7%BKO+DX(_kk6 diff --git a/jackify/engine/Wabbajack.Downloaders.IPS4OAuth2Downloader.dll b/jackify/engine/Wabbajack.Downloaders.IPS4OAuth2Downloader.dll index 40cad4ee2ce8d09c83fb746327f3d675bb2107be..3a18af12bb8cbdcc7d33cbc0e804dbe8934ab7c9 100644 GIT binary patch delta 435 zcmZpe!qhN@X+j6fYzFJzjXi3<%&ZIylehXRivwvma1w?vL2~bXEx~HE{ZzG))!b!- ztAXoN6oHFPuJ-FhF_S|OF1K0Q-&8>$qkDOA!seguO_pb+?CuFY-<*&?k=xQ#&p^+B zfx(!O0muMyw38Ao(^6AS(=3t`(-IRc(o&KwEKO1kOwue&Oihf^EG-NT3{#R*(>7~W z*RinpG^{x^c};zQfCGDIU-4v1-vvEuF01-5?wQQfFiQa{xE3Y|RP7Wj8yD&$H%0oO z$6@cy#~KbX^22-yr8iG++R4aj%Am(!usO8lI%7DP$)L@U#E{5f$&kj73WTOW+JYe& zD3S)mi9mK5Lkf^@0aRfEls5p9AQdJ+l_m^EK(-~2X9yHG1glGBNSoZ;X2=OK8)TJW MhYRCo#*Y7t0Kws5ga7~l delta 435 zcmZpe!qhN@X+j6f`q(=UHuk9bGP5vnOy26NEDod@V8BTj!UD;?_q7D8(e_i-Mpko| z5v~TNjX_ZaE;hN^uM@>g4nerwW@&#@1pzPaB!^|^-h1~IM%+AmdkgF4g#3xzmL_@@ zdIk&(#*7Sl26{k_wz0WEl3|jmp=C;1nvtPFnwhbYiBYO$T9S!TqG__3nT3%_nyH0J z%4V(VIu;h8u!XNCuc;3Z*j9abkn+p&SfQdh_(Aos6s|40;R}n?qZ!Glqki4B8CF4CV|53`q=z zKx_&mEg4c6(tywi$Tk4dWj!D7rb`Q^r*1su$*3=EU?IhE~!G#uEmK$ys4DNtEtu_p+1nWB@tZNr{$esi~%E7RiZeiHR0z zDajU=CaDG{X%;4?CPrzN7KR3fDaom63=FJ{3``6R9E=RyaK|w)eBZ3Ob#j4VfIwco zLBhV}R$g-!F5z@sc-ngM1;I82sGw8~R1m1z^!WKWB?H#-H&rt>>u)X-5@O_vs zo-Q(xk=2wzkHKKGuGnnGa4?fWn<0rIk-?H7jUg2XO@XupLo!e#4Tuwg>@s~yOCZk>C~gQ=m&%YfSzf}B6Jj>VDnY3gjGGyy{xbpq6pL4q delta 406 zcmZp$Xt0>j!E*TFvS%B67H}}LFmO!P=Tx=_(hM+Q#{yv?i={v%k;R@M)NP)~+0828 zBdHL*?6{Tx#1q9i>tbzGH|y|!U<@(Qv(PhOU@&H6&@<2ja%494ZJ>eJ7^dKdsf#LH6^O)0@kLgqZjtF6Ur? z(wnD?Ok`v=VbEi+*sLoyn=u^BWYA_XW-w$p1RZOK z4Or%ORvmOcc0yE+mZ)e$1<*H|w6I#wi-g2LQ*J*dbt9Jaa&qxZ7%Dz$!A}W^1AxZ{5nMWYu4{bSeShT8i_0ng5{U;G2fR z4>Ud{_c&3UJ5z(EH}= zlpAG?xYJg+f#_i^I?WK?wJ*3aOxi0s{e+I{35-#0MZQ~(C(NB(X@xo0iQ};OkyDM+ zh|s#$YGrIZFk*$USzdRX48DS&vECQWZdO{S<)7PXO=&32LhBfN$|AcNt!~_QP$DYoAFj&a*xgnT%k*M8-MP*@9H&O@-eCpYSwTj4 zst?*%xLJzsk}#^>E9Oz2RRK&gwcUypq6#nBB~pbOb|vviyM}lZ_2OWhPkMJkrF^`! z-dP}mG0iUG_gE|z_UBlatd%@Gkz*fXqvU1qs~nwU4oW`M;{jX@tPDIVg-A{_EDmfZ zOXb*)&3zKVKFzK%T2D*5!LLOB85u%MUEmskPXd#Btu5ppZ zWg1s$T&FRuagb;uGt8kFTZ!cwi*cT^@bneFgEpzhDtX&&0$=Y=3(|7aO+n;uP8W*k zvkrr@BCwH;2ypbolYYg??Y*sM9^t|8RQ5pA|~_Ukq*VFNMmcZiHTysaM0b;(r`T`5oR5rvC1& zlBp#{MdGK6%lz@;TK~i11^)5k<@}uD>DXT`2Yz&8#i{onIyq~}doTI-#Om)e6A0Nb zLZOh8FvJ@U<%dOzy+Mg14l^i*<%$;!0Wk|NA%2bEhi zY%<}~{MWO1*~@XGFlc(KNM94L(+gM$F+zb;_ioHYei!^);KYktQ?vhc@&VT=_ST5! zpDc-F_Sm|c3KJ%mMrFWT=iTomY_-nFgWj;$8F|Fp8rk6{xRyaN@>s5y-Q{KX6eL*0 z9?mtvEwU1b^v_6G+DApHn1aOgHiVQSpxyO?Rq+g+nG~~XdOyA4+!CIg=8+u%&`Yh& z|9a`k_O|9^Gk=H9=leC;OwzO|-9FIY-!_oyO%J5gse!)URC{ZGvUMQU+SWR6pgq-+ zZ0YOmAMlTs)y1ywy>STA)va1;AMZ2#334XpUoDF{9sC*EQQ}`Os}67IIQ#70$%8A8 zUw9$7IJ5bNKc{@>yiY5cs&x6p?n3ukPuKGF@l)T;{fuQ+Jg?-$fvT8sq|N_#`8GdP zv2Dh!e$S0l->=vub{m?JoSGFMm$h1Ci*Mp5z8wST=U*Gg6klX%j%og-IQEgJ=+R1h slBhgd>Dh{Tl-tQ#sBh7J{TT57Q`wT6W^UpSVw`K6id6qo4kX?G0^DlhO8@`> delta 3830 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 ICnNU%0&q~EKL7v# diff --git a/jackify/engine/Wabbajack.Downloaders.MediaFire.dll b/jackify/engine/Wabbajack.Downloaders.MediaFire.dll index f57b1eed421611b42f8fa733a0c2cd0b2e6ff1fe..105b3408f47004967ef98822485b9e2ea3dcbd07 100644 GIT binary patch delta 430 zcmZpuX{edd!D4g0<`nCy^b8mnj2Rh#3?N54 zDbX@5HPtlDA~`WFG0`F|CE3E#B-Owq&BDag#3;?u!qC7lB{?;XfgymA!F%#Qb+O3> z#_F5hjExvso)^5HHo3+mK!E+l@kb61K0D9Vz3|ZTOqAf{J0>j(P{GZzP(hIDGXg0^ z`&O0NTdf!1-P~Zx!N?EuJCxo$-K>+5)s#Vx!CVJJQT73>=fQWt6pnGy@EL;)Jk}#a1K4ChwH7L{T5fh^c-S z7hG(ztgJ7{p3S+k*?a<%9;=$o{x;ifZ{cMNDHg3gn_p-@VzW2Vv(PhOU@&H6&@<2j za>N5@7l|gYfJ(JHV540V*jS+Jh$}hjeY*{{gdyQv?xFYlVhQRAk}up zE>9aiZ0M-03EjN8!IXoMp9SoE4hAT_dAeCABdZC69)rbZRf~Sca4?fWo57gDoWX!0 ziNO$vO@X8(LkdG05E=p520+>jsKSWB1c+0Ca%n(SAa){9Z8A{KjKKmdn+D`t0A*4p Ob6Xigt#P#4&kO)GBVhFa diff --git a/jackify/engine/Wabbajack.Downloaders.Mega.dll b/jackify/engine/Wabbajack.Downloaders.Mega.dll index 4a101160c70d2133833de24a7592d2ab6327c5dc..4feebb718ea939827ac357eca8f49828542c36e8 100644 GIT binary patch delta 434 zcmZo@U~Fh$oY28iWVP$x#vUCxW>yA<$-Cv0#ep;&T<3%^L2_T^EbWoyHX-ET>O&df z>fvJbTyU|;x$?eXd-lq!g6Pdp<>mPVW_%KiUNI-hb#Z~!#-0{_+sz_+=h!Sw^$he3 z7#NHh8GsBRM>{FeGA%XLG|eJ8F)cCCA}uA^!qOzwz$DGW#MHzn&Ca*G|wLPL@o5AECW>pGM!9CefL7-~3`J$>1_oS?` zd#uK-GI^5uH-4CBq4ehI7L|;wrVM%v2Af5#${EAKOa^U+B!)x=ONKOtR3J13(iRNK zK#?>cP6V>k7*c?I3!n-Upu7Q)1gS6qsx)CR0TV)a~bvB|mezF>Ry%BzCt%}?d!`2;E!z6&u{4|Ce%dS-Iw)bypBMfA?GS(@ls z=ov6D7&9{H8R!8y+Q#MvNrp+LhL$O5X-0+yX=cVoCPt~2X-OtViKfYBW)?;!X{Ht? zDVwEDD;ZhhzP(>Qd4^elK)8-eN~Bt^<09`(Ii+uX=1l%!R;2(HG=vEPRjY5*yk2>` zen+_aijR9tPP~$U?mTPi7b|c5SzSK$r44qH4~|fE?|lM9Z|)RMRwzXixMB?_TR7DyKm|D>p@JaQGXuX(So2v-nn6q7 zZ}SzWEsXpyUqk85(_JbVSxp)A7z{Rxx|K7AgP9E43`q=$43-RO45>h93ZyLSAmF delta 435 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 MF7ZEB_Vyb1Ka?}WQ2+n{ diff --git a/jackify/engine/Wabbajack.Downloaders.VerificationCache.dll b/jackify/engine/Wabbajack.Downloaders.VerificationCache.dll index 0ff8b8ac433e4757055c5889381e5ed697335051..4c80e62104d34910a2f9614df300e4b539de8816 100644 GIT binary patch delta 348 zcmZq3X~>z-!E)fJ$IOjAA!5v|3=ESWiz$l(X*ft_hcH2Mvf{oVHJfwAt@#8hXH-v` z&GO!9vD_DXvptGCHt$fCWU(~WGte_&U@&H605X6a?W9D@wA577G>hcKw8TVur%>QWifCPy0?Lamu-l+O$Ri{w~0 delta 348 zcmZq3X~>z-!E&}_!=a5mA!5ud3>=dmiz$l(X$BZbWrwgpaKGBijt zGd40YO0`T&GBHXtO*S*LFfvIqwJ=H9%%v5@$Woi0yJ~WVc7Q<8N#_y`yA*U%oz+Ak{Aqu*c3=wGNdr10ih9)Z2+XrfGUg_On^8QD3=CQ i1!5-x)g}Yw%or@dvS~oR1yCksahcKw8TVbGOy)$t7W33Q)l)m>^Jf zq+GuWo*prik=2wzkHKKGY1DGYa4?fWn<0rIk-?H7 zjUg2XO@XupLo!e#4Tuwg>@s~yOCZk>C~gQ=m&%Yf X**M0K6Jj>VD#5rZjGGzb{xbpqv$bhk delta 445 zcmZoTz}RqraY6@6|Eb!>jXg$&%q$EXlMfmyivwu}7|7s;ut0LZ4OPKvl#P6~k=2~z zfvbUOV@PL$i%p(wWQk&?BErnguZwxiRUsq8cMT?^eYxg@Mh0V+62 z2Pz0s?eJ`^_WL{r`Lc%`p_}Exvl#hVz~13tfYO_%N6choHDSZux z+8;7-wQ&6lc;I4_Lw8z&^lzTH^K+1Z%k&7ry(jctR{ZHZv}KXE&h{xhjQ6B1P4x`) z3>X-U85w{KAV)hX(K0PH)ili_IWa9U(IPD+*}~E!)xadp!o<|XD9zHs(7-SyIW=wj zY7NG-Y%KfZ*WZ|4qt6&1;35;pU(R~lWzEk`Q&-;-T{r!XK4XajR8-^*R1~CMPo`bj zQAGL5wgr__w-*>NN-*)myalDVPd8%BWMnmE&|@&z&T7IK&lnD7GiWm;F(firGNdu2 z0--68wqQsGilhN?B9NWNkOJge09BX(s)f@~^di!)E#!N<569zp7i|wo?jPZ=&U^as`gE50S zg8@SlgCP)`0!d4T6oxb)Gy<{>fV3G_nj2WT2cGg9TVN4am0u T%A`!+ZOUi}wdkWMBR>-Wk#S_S diff --git a/jackify/engine/Wabbajack.Hashing.PHash.dll b/jackify/engine/Wabbajack.Hashing.PHash.dll index bf18fec4b038063d55a9b2d96e9bc4b48f374e81..8ca590a85b28ea5a76fae633e36dc3132f9d6591 100644 GIT binary patch delta 362 zcmZqZU~cGOp3uQ!YB#ZQV-HI{Gb;nbv_&9Hklfk)M6epe0!t9RIk&*t zL_i@wQ1Ha{$8PH{ba9F9{62g0jwQzgEKT(c^b8mnj2Rh#3?N54DbX@5HPtlDA~`WF zG0`F|CE3E#B-Owq&BDag#3;?u!qC7lB{?;1^1rPPo4;@U%EH1CezWN9E*T{YT*Cz+;E>VCAe%FQyf>hrLa+qSEDyI`ySy#ep;f3}}l$SRlEx`H5gPh6R=&dUI}p zwTVFQtP5Ok&lI~(z3}R#FvrJ}n|CZZCSYlzXQ5}nz+lYCpl6^5D?s3G z)RQINx(vQ6rFo*+rc5=T9I(4Y0V=2q69lP#nRHh-w1B@x&-moN%};hOVd7^2JD!69 zN^hRNFO!kggh7wNVl(T3c*bxrlR=xon8BREfFX&&5Qt5Iq$NWNLmCho0oevX+6<_| sh`|JiQ-N}6Kvf`iB2aBIP|l3O0xX*b30>w6W>&prVxE`C76!~|$ z+p?8*c`5$Uw>Iyvf6ifVs%M~Qz`$V4$N*#jIoe5ymT9S}rfC+*iD`+67HKKT7M3Qd z1}141CZ;AvX_gj-28Jogsc8%hc8m;`lM6hgHvjM_VPtvHmNIj4fmeV)(jw0_#YO4v zyF9*M{q?)Nc=83WDg~&ZxfN6pr278<$hx}3Sywl_z4>KxmbV-uKg|75dh>LjN=8;w z20aFY&7ywgjNxD=gEm7FLn4DELmERW5SjvM3x;H%NE#3)0@-N{DL}pjP=yIl-T+8~ sRG0u&nlKmv*_J?_AyC{9tS*%yZSrG(Lr#dPx# delta 361 zcmZoz!Pu~ZaY6@6*U@92HufCSV`gFCm>j6DEDod@U|TlTD6KzTj1*02R!E2?ABuY9*euY)WqVa-qQC&*m&|IYxdKu#-6$p!DYH zK9!8DCJcHE7Mn%=${EAKOa^TRV+L~u1BN69Lm)N*L0rNeaxY3=EqU6`eT+Zhy<&^m%f)?;6*jvX#GV&TKBxie$Ak z)icmDU|=w2WB@XN9POk;%e2%~(=?0Z#I(dji?ozv3rmw!1Culh6H^nTG)oIZ1H+W$ z)U?fP2G5vSbh13>PX1sNAP^qBZ0(WSZ$6W|)k>VZ1>z?M7-uO!1!FEl1wpEl{nWpA zf&rA?Jl!mkk=2wzkHKK`S95p9a4?fWn<0rIk-?H7jUg2X zO@XupLo!e#4Tuwg>@s~yOCZk>C~gQ=m&%Yfd9tM; TC&X-^Rg)b}Og1xG{bvLKcY|7_ delta 320 zcmZpuXsDRb!E)o&)8`v|CMhtpFmP;ERCMMPxE@z-`P=oa*Jc|YHqCQEOE#BiMY39& z=vn9)FfbT1GUyrT0Xf>n<_1ZINv4LDDQRg&h6ZV7#zrPasg`L;CPs;-$!2C2MkZ;d z7A7g1*$kdBvB9)n zG2ZR4KzzVTq0P69*D>+4Fn|CD1C-u8-7J!k)r3Kh!D91Qb9csYFq1)>!I;6E!GIx& z!4QZ|futov3PTzY8Ufh`K-vtb!id2Hh*N=bX+TvVb|O%1GEmNp!2&Fs2IN}+Wl|WHY1He?|b%YE?@B diff --git a/jackify/engine/Wabbajack.Installer.dll b/jackify/engine/Wabbajack.Installer.dll index fee334c4adfacfa48ad1916cb9c23e61564b57e0..7d60b3e25ba98d8c9837db6270815f2657107eee 100644 GIT binary patch literal 142848 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

P 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≫^b~*Z| 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 Q?;d!RPXE&O{}&AWAJf3u#{d8T diff --git a/jackify/engine/Wabbajack.Networking.BethesdaNet.dll b/jackify/engine/Wabbajack.Networking.BethesdaNet.dll index 4783fda4e00c9a586665a5ca030bf7a3a078ea87..b1725b54cfa6651542a416d87ed318830068b298 100644 GIT binary patch delta 369 zcmZqJ!PKyWX+j6f^cgD_ZtN)tVP<7unEXFPSsX~ifrt!*36ir6oeNU4`DSRlhQN8F zryL!3OZ;Zd+-#=*E>~xBL7gm@y{Vpoo&f`cF(U(z0pw^WC0eGXrkbW%BqydNCR(JW zBwJXTq#Br{S(uob7^PWS7#bL+B&Vh^Fjz7&m`rBu(%k&DV>>$wqhj#$$r~mF2pvp^%vEUu delta 369 zcmZqJ!PKyWX+j6f=@lD4ZtN)tVP;|AnEXFPSsX|+z<`Jhgawkb44n&7v-xIdyN19E z!z~A=ewydku<-uD{ofj*Hy6~&a@m{cS?C!sFc>p3=o#n%IoihN21$lVriPX&X=z4= z25Dx-MkYq7mT5^QMv11$W@Z*fCTXS?CMgUImW&K0lNq}-H-GKe&d#!JcE#4o8zuw@ zY!SP*CT->cx4yH5_m$=awoMk8*ros#6yt*m0#%#JWP4v~zyACFU4`1|o3~B8#Kg}6 zb~*HNl?FbQT>28PK`CA`73v7{=9-W)Ay$tzImqF-&g>w(vjS)nn$ zXH&~IFHs9)u{YH-&@*6QFlJ-`GJqWIq(sZK)Kt?ni{!+##6*jDx#AoXsc~5(Pm*{*%E&H;p7_K00HAGQ_rNH|LL>+ zb>oRruBMff@95SkKm~Kmp@Klwi7R+*&f0KlwrqU1T6A-j9v>4w(7j*)r8iGEXk=tH zWzb_V*eq+*%oq-4GH5d-F(firGNdu20--68wqQsGilhN?B9NWNkOJge09BX(=f6N_c~5V@XvIy*XOal2?H5lRAHp=wFZdJWCdt zw^dx(yhJUG#ok2ELeGGK!I+Uj&p;2z(Ka?WNHR<^HMC4gOEWSwNHa4wGBHZEOiMB` zN;FM2GqW%wHtGvrCr)wrN_s_&%yu#91Ku;^K^qo zMphFBJqC--vPR8};b10%HiI#PIfDU15`!TSn*vEoh7^W0AT$E94S=*6P=yhL2@t0O m<uG%!p_PEDKq+Rb!xrQ2&}mU8}IKPG2*1qf&h+ms8W%Q(%yA2ad&9plB5 zPk7ZRKm|YDhYA8!S9Bebx+sxc?Rji<;iJtd-YSgzKv#kRl-@kur;?G?ltGWdV6&)S zIb%4O$)L@U#E{5f$&kj73WTOW+JYe&D3S)mi9mK5Lkf^@0aRfEls5p9AQdJ+l_m^E lK(-~2X9yHG1glGBNSplF-;fhxHpnW$peKx*8H4^a0sv<#U)2Br delta 343 zcmZoz!Pu~ZaY6^nc?-dP8+#ULFtadlOxD*l2h*9FmLPibQq9jI0)CpmC(PG4=(6eP z>%_L%A@4S?uuEpQG|{urGhkpaW@OMa&;xR`jm-^`43kU^EmP9cj0_FZ%#4jpj8ZMr zl1z*eO_R;cER0OjOf5`OCckzw-CXJRnwjPQ)u(GGXLtn&@S1-5oA9g0aaW=Lv?Z(i zStg(Gs!@OnF06tI0#)mVxZX8lf0TJBDn6`obBebLBR>lR2yie!>CMx9Dj8W#81xt{ zHjDa|Glqki4B8CF4CV|53`q=zKx_&mEg4c6(tywi$Tk4dWgm`D>%g&Elu?d^b8mnj2Rh#3?N54DbX@5HPtlDA~`WFG0`F|CE3E# zB-Owq&BDag#3;?u!qC7lB{?;1@((Tz7Ly68`zNb#2MEYMGqTU+o#-@gb^GoKzR&C@ zw{Z6%dJG1eZ3Q+n zhJ%?5+6+kyi42wuX$+}AXbPk)7?Ob^X+WF^WT!Et0QnX`6(&G=10V@fVFFZX!e9hs aTLO88KygE`x>Sa=$?igiP-|+1nwbGP+g7On delta 346 zcmZqBY0#O_!K@d?va$O(3o{D?$K+5}WiVaMswxiTGr+(^W(W%?ws|}2LuP?%p>-!b z*GfBYWhuO--n@Cn<`tacjFu*P7J3E@491KMdIowxj<&J6L6TvTsi9>`TAGoeL7JJd zk%>{NWm=MnQKD(GnVE%=Nt&sJNy_9OTpBDd^L(~VR^bj12(EoOZEo3l-+5P}wErw> zI5fG1yH5cs7#jx_1gbWxabA1&>&ypd&Em|(HyiPUG4Zp2-O0fKr8iIKTgb?2!l1`s zvDsE&Gh;ZI$)L?(%wW!7z>vgX2*jp9(vl&CAq@zPfNTRGZ3a|f#9#u%sX)0jpehhM g5vVp9C}+lC0hUbz@-2WeDU;oW458N43Nb%7 diff --git a/jackify/engine/Wabbajack.Networking.Http.dll b/jackify/engine/Wabbajack.Networking.Http.dll index 2b4783a4b6c234cadb2fb99c9eac4eb0d66b730a..1e00078028e7ec4e3bcfafbd8359f7eb85491489 100644 GIT binary patch delta 394 zcmZoz!PKyVX+j6f4eQlAH})OHDOR zvq(-%OH8y#OG&n{G)XluNwY99H8D!Fv@kRa*K`I5 zEUZ0uuSxl{Z@2fA@8=uOubIr#RiOYC)IALq1gg$#)NAtKTQZ~9B{Ox|<|SQE82Mqo zgVLL)_Y^X+nlk7y7;NV4%VrD*Ga0lQk{A*hEE&=mQi0GENLw%@14Yt+I1$KBV@Lt= zEr2Rafbs@F5~RWesM3VN2*|bs@(h9EhG2E63~7_k_ZxCT%m!H{IN<~1L<5e^jFbK| F0s!LmZJhuB delta 394 zcmZoz!PKyVX+j4}-64Ui8++6|nOPV(CU5mD76;M{FwiInVS(iAy)04W8kiAs{a(Hx z{hRN5p3=o#n%IoihN21$lV zriPX&X=z4=25Dx-MkYq7mT5^QMv11$W@Z*fCTXS?CMlDx+ub)?wdXOh7;`1=p1h_r zK;XrC70FizuX%L6?hs+nYx9`Q(^a7W72M$k6$Glbep>L<@XN>J!Lzr!I;6E!GIx&!4QZ|futov3PTzY8Ufh` zK-vtb!id2Hh*N=bX+TvVb|O%1GEmNp!2&Fs2IN}+Wl|=e?>FRxn61ZPuvu`z2gZp8 L9Ge*@{bvLK4Oe7F diff --git a/jackify/engine/Wabbajack.Networking.NexusApi.dll b/jackify/engine/Wabbajack.Networking.NexusApi.dll index 8435fc093f93f834a9812e680653e235acd7f3d4..fcdb487a27b578db2ee437cfd0ccabe3ec8561d1 100644 GIT binary patch delta 31579 zcmc(|2Yggj7C(O9n?9*ClSwZ#nN0H169gnQ6KMto3!$SXQWQi76Lf*e_!2}E!TMNP z6*bXV5M)JI+p?NyM6jdS0h1LAYg=8#vTXc+&v`Foutp+Dxgy`Sws`^npE0yGAOOhHH;V1yZ$kP|8i)0SokV3Vvl_5pS;) zqU|*yqz$bnNGEPTLw?m%hTLYikRvB@YKp)O(B8-pPMdYH0z1WS(L7X}Y|SpKF|wR& zWEo>*sn)aTVnx=o)M&R_vqyg^^iuxGsVVXmr{>7loLVCP;?x`Yg41SQT`}nBcE_;iX7(D9Qg;QmdK}^dLtik+N`T9X5GZc+t0EcF%A8WWpdF>15W3pArsI+WU+Qg|jV&>Ev5u7&bYD%1*l_+Jg5Y8gusgT74 zw1YPV zq;qPHq;cwvq;T4-t0{?k)>Q4~*zB?*7RixfPEC;#BTJ#4MHeeBJxh_69`{Jucq7XM zBg-j9mT`I(U92SOS;lL9aoJ^mXXoX}2b`KBhdA{{dO2;@#fn$Y@^@`we0Kg@MwYh? z)i-rjovkG6s&8r6LG?7lp-F}#r|ORAY$ZikJ#G7I@w24SXBoOvI5kD;I5kHwjL_9B z5sVV<@@I0|tg9>Ox{b58FG@5!rE|B3(ubJx=dy|%nP+4pvq1;{1TEFYsolk%FHxvQt>Exg-u&m zJj9LeEUv~cdYhPp5%!!(d!@KUzS^Yyr+9|E*{V%1885GpwL41+Vh%t@PL8||Q`++- z+j92-k(*xxbjQld5IAt)LJL&FB=E$_&HJ?5OAA$xCr)<7MKDd6x~GY3Yg_0c{rO;vqhRy~GF7=GUsBbF#9q6}p^#-yIJ z&k9@CCO=e_T!7?I05CsUmxjq?$OTS_j5Xx}#2$DVvsJV_2(GdnCQBz9C4E69slO-+ zUjDMEl%Jb2N?AyyBt@utje}xgFCWFkIjYV^XAsu(5#3y|42Xwc3DjW=dw?s$P(P2` zJ)esMu$S z4<8d_W^p!N9w(MG?SSPKwlL{&QB~3(wVCCGrt}fpv{wfY*5XPNeM#;lB?&nedc20E zQ=Y{ux>W{N`a6b7Jp*+U2QZ?AC&ln4rO}ztZ&G)m zaRV`^F+HiS)T%q7?MiLj4rY3&l(=cGw32GWpTB~g+CS7%LuHV!n~i~YS1`qunw*p5 z(s@d`G!+e40r6ejxr>8_fjU=IETRptnyt@@{m3sX=t@iOlU%7ydD{Jzg;Kiq&q}Y^ zm9A83pH^n7gHTElyslXffS)^1>7z(*MaM)J6f{>5|5MNxBb|wkjQ<2p7{sr{RD`8n zq*;Z?N7}DH9|OMzdF}-ty@>hL*Qs}Z4_*XQXQ~imK{t>@E;ACj1KAV`7 z6Lc>#DlFm^lwQVjE-f<{_-|`l2hFJ>LU!3A?@-$O7NFZED^)ZH)gp%+CV|H$H&fc@ z#lX_%pf|NKCtOj0U6V632*C@woK4s}IYWb)h-t$9${E5`W(_ue?$SO#A!WwE1Tnq| z_N_s8Y@c^D?73of6YjV^?@2_*=~mqFeco!K;|)XD6f6nB18eL72V!DO2-=*%>C!Om z4}*&ncqG#-tB+)D+u)njC>Pz)E_$L}q^ZTQ{4rw4lWJ5KHM`7Jm;{B(9Mwfj#{4hU zP*KmH*JH}DdK`@WFE)O~#Qp&j!;@kU4)r7%LoKN>*V8}Lu(x%VL7L|A$~k=zZfb5~ zmshER>|oOIY`ZdC3k|KCMZLhJjDQAuct(XyKv`f8zns)MNoaqEQ$ar*!X;x@u2hYtZ&Fp{mIF~|_-HYsC~mX4H@;XJ9!j>w0U z=vzmVtc*fc51uWDi5n+3Q_|-h+wXa=P!S%Jg1xCTjD4#!l#GRzDq*HduthfIGAr25 z8kHDe+m%{y(cls`&qBnDiDfihTqTDn_Rit+2}Hq+<%& zS`Pn1`zlzVl9<~1GfMSC3fdZ@zQ3(WKy6KFR9jQN=(eWQP6(4jaR^&py{)ko?r&?o z6l>&jXc?uTt*iUnn%LCVl=gYY_eYG&uqu;b=U7{_CH50-eL5t1TMw}9${E_W5$OT+ zLWBfc=>9Q6A;CBoZ<pFAO1%NgSv{%Z0-4{}-b zYBYvZ`nTnJH*r}*4XB=eKr?#Id7A5_OjVD0e57w6=JB#Jfnts}M9fLxiIxzn2<;b|d!z!Ky@CdcBOq$WH@h;K+v|SbR{uKn@d|4t$jM zc`=Wgi>7t9m!x+$?j|mmg44P4V8*XX?FvrvG(u}KQ6v1s?0AjF;y!x;Nk8pg7u&8|-WopJme0{(wGj!h`ze;dbU z()`VFOuo`Mwo@)*dh&#V|L^>$Fc5+ zGh!n<9^H98(%Kj@gmlAII#}@#C0T^=Q1~#xZgG$1!W@Rpk;dpfl|~i@7IA6lQW`a| zILz_Jz#^+Ouo7tKu(_lN#Y?y5N{AX*zmTGL&C}w>=JUPjaRcZ|ix5M204+enU;v$u z1be`Xao3eT=0=P^TQ~`$u5nc=tz;KK8d{Bk9$E-Yc@tRjB00Q>*cSqLEN+Xkm=M|z zTm-~ey2!fhQq7Tz326=i(zg0E>S`AFAcsjnTMJ72yp#Hu49W;ommrD5p0g3Dz`hb!>Nj%UMP{qU_Xyn2NU|CdTyZ@@E|);HZ-TjWw? zH=M<>6FwCmI29WQIZOgNc2XKOK-pQFzR+4_mo;*k_RzS1dgM6AE<>Ks@gsv-_2BEy z$zf*j(tH5j*`O(p??%vN-e`^nPBkWdv{W)$D)a~P%|38GOj7$y9tY-|Qn$-dYHOU9 zCYBV(Hr7*OY21w@vJ6=allti61|66-V%#zP%Rh}iJv=dy<&;ft%NSXiOyNo3eFzCW zF>*7deO~q0`p@x+(bxa}Yb-e(w<^Z^A62b^<$oYJ!Bja+uHF?)b0Ji`4@hF?hiyUq zf2#&6;^v8}0^M%@P8BBe%y)bhezgX%#tvpk*1!(t*J_~Z&B^VH9ZYQh4u&db%zRjx zxti#p*2oI*{ z7>=c*@%QU|iYW0Z4b5bMzD?mUB=OvW+gxf0B-{$@BsMMAfOoKvX5RmzYgg84FV#*{ zX@WB;>j0_i;DYPVnB<1?L`!c$Xfa&AH|vZP44B0>b1IpwVzVTmZ6>9C-qQYY z&$V2-lr*f%M6A|FqjR?&uWa3W8fg%BaC#o9S% z1m$~kv@6fZSBWRvBjuDd7Xi9unVwCG4;1nEApss!DTx|Bl*=vi@G;i#T^JJjqtZ@v z6tor|g3O^~Q%eM|K4!TIyDV#@j8H>CC5=!HvPAH(gZU(-Y_2tR#8`BrVN0nm$zkG$ zGD)Kn7#4c6EQg6u@+o&4>dM>Yb|C1Yv;pO6;UaDj>FV{uZg~dPYYLAT5@5GHu9=WN zFFLP&!@@N)D|f+E|IO9cs9OI8i(W1-DC^Gezh5cUzBn^)_-VfA-7~rOb2YH^hb_MS z8mNhO+F4oCb&su)6QE^yj7z8JWyqn@kOF)MnM;hw7SGLGtXc=B%BR`A0dX`5D31}lvX`dGX zjtZkq!R+qLr^|f3&C0a1r)2(impgc_9R9EU4JSA|?IYN;B)v0n1H0_X1L!3O(|=W} z;GE+us=-}{_WhI-0@Tw@3O2p^D6xrM*gg#7Q8EM~jFJmd|?Q&)H!(nyuIzb-IQw`hxu9I%Lp&LNAR@`xh= zMU>J$FSa~U7Hx`y8i|ia!?oI}+3qT6Rege{Fdk{v`LyLzgY+y0za5cLlVIs=J9IP2UYX0L0jV>*aVNCk<3r z^~g#CRd+X!Qj&N)ax9-YDP7P1^4evpy`Jb$82R=O`&Fb@`c5?zUn ziGIwc=Mm*H=2ss${gV)~phH=4E^qv+U&YOLqVi|hqFW)?x+`c_;)idStF*|KcwI^V zGkF(G^xxbe<3K+BG9C*D-g#XiEHp^D86@aqFkTM<044}}9JWruD&qrxQKhOCMwh-!#1L7a>P7d?wP7)Gc43-kj}#7|ze z;s&KW0#JgJF7@x&1Feu?(j7A$nL`NRuRae_0l2EcB8V8&0J`x!I2XO);4C>zTx_i< zi5gCng_L&})^K(W#26FKDC3t{sM6LfRaN`5Vo&u?~sA^lqV#IAYS9=^psP+UT zG1jUzs0gl+ex&;7?+};e8r@69CsT6x6zGMHQa9CCJN=<%2b7{@d;#XR^m+3Tb(ck% zhf3<}5Y3*(Iz%rwR5;DUwUjoG2c+At`A|RbGF*_uB%pa1X%Yf6qkrI0Mwq%s+chg! zeH?9}-`mq`vT<)u+XdacH8OF)JQm*t6!K|un7Fj^QW~`hV$)XM1PwdT_tQ_9w6QL+ zDfSRgYEPg*{fUv-m~P2)yYg3U$?QOYaFenVD#lLDkB&NmV|^+_i1j4xk=gmGFCdoO zrC=mFBa^|mDcIjzLw^I!w}npv!SqB?qHMIyKb_L%(*WHTS;0P~_y9&MqC^6m1ez)B z^I|#BXD7;sd&j3>DVEgr7spPJ_EM{wH0&yb*d96&#o9xafV@lSqH<&1_CD|Fu;a29 z7eoo4hVb%d0Ghf1_^3?g_{PN^!Wgr99BxNYc^1slxO_KmE3n37xSYivWf$e3>!#-@ z$L=wJ_RtVyY1%`K=Lx(35KIZZh(x&n^}~pu*z_e2Lq`f>JL7cF_a@w};*^)Qtg};9 zJvy5jE3{Sk??_KeYW6?nR-WQ9d z^Ps?^JYO<%f!qw($v=x}o>1BjoL8VHHkY|fEjuOeqrOwf+Y4wuN1usIH+N`=p>GSUb4%f$jGTLKF3S7 zgXb2iXTiEjF+-(jfvljnIb~C14w$m?CYX_PfEHOXygm4;A`y*%S4F(Fkp@~8t%8>2 z>kwL^a7`ed#BXwu?A49fs8OtudNLR$=;J%8iYmG3I|zc>>Uy_I{LnX~cQ)yzaMqN5 z8!(oc(}x&Y3k+d5c0uc7w+WLPw4jl}bga=3`~)da<${ zcWqqcaT)bOPFnWUPd_CWWy@jmwkW;nZ4|SjpXBe#G0%w6$KsUJcLHv$DUck}6Jl_W z#1Y0+k=!&He`{cMqO~cO5dv1&Rxm;kby~LDVeBL}b{>QX`aEq{Ly1ZRj=l3KX`Ty6 z$6nk?6(2lZ4igtM0VPp00cA1Hya!X_sJpNc&h5~T9z41SxcZp`4)iay7hEP%cz+Sp z3ym81(Vi|qN$~Un_LK;Cx{#9Q^VtWPzPD#ji3?9DiSm@P9PcR`;oQ-l4sd_lQxgi+ z#?Gx6{Sa=a0+$PsIaP>&iMH( z3(EMhiU(4=3asL+G=h5>K9Q19;1T~QAnEc{UxrmYmiB?q3gZO-mP+tlhK-P7v)m8z%5QpF%??HT4Z*Mjk!ni zlSOOk)__weMW)zr|h}Drs=E7O~6}MPhQIKgBvvNMnc8h~a zNAj(r1$-D{)78mTr|W|R=lLnujy%HGO9Wp4o-E!e+ZxChZ)ZG=Toa+0FTQ5{7S^d| zcnw)l(PWgZCW#EaCO*tg7GEQCx_BIQwTid1uZE`w8Qv%LS&GCI|9ZIlFhc4U6A-O3 zvDkM*<{A^G+qt5Am| z*^8WFKAK>VXoU%gGf1f;k6a1CT(YmX13T+_+Wara0DMr zA1cxcso=T_(uuIn-XdzsQI$vX-FR#`l0RCEEW8P&JZJha`(zPPD?fnOa{8(8WfX#$ zEXvBa2F8m<^88fpH8sp(=XQ#&mrn=QJ8gjpb^^7Z^0 zX)gLAQHzR+T0|7`CHbpJ^d@<+AJ}F_fumSoVb&#?&oL9TCHsqOaw;unvr53Rh8} z9MWi}eq>pSRLxYXg=B@QbTX6AqGnzA9&;>H|HL+-OH@nanfiof!0!_Ocrp^zeFtEE!u^1O7&1LG%{KbXp zKY*`|d(CUL-e*aWr?tG2^EpCRqY&)KA}BiuZnF~9!0*@0EB^|dWf=sQBon+ioxGYB z^DUrW)|qiX0)IH3N^f%$9E`FgQR^hl7vf(#1<4^72_9e<(^;?~o|H^zJxO$OOP=Z? z{3{OD9f>4e&-#a*gx?YSnl}BSER}E%OYi1jy%R@OJ(58Ddf2pzxhV;RXYH}PZ)T!IRrD0yfC9$K6qXB2)T|w1Q2c<8>x~TP zbD|xZHu)Zo%>(S#4!{rPSI}6hiF)=j)8D}P(X=b7;FphC=ny-i_-_GM^;Wsc`z`8y ze2qVjQvJ>_F`)BPlr%o&*P?j5wJhlLhrMJ#UyTa;@fk)$Wtey@!v&Z!unEkfrulh4 zpxO_Lr`kt%&)?6hK7+1Aorp3=XZu>}uM_py{(2|KcSCcwi8|n$rlWuz;u64HWU_Oi znVN5ziLCv|uBpxYeWkP5*e&$CM=I%apg)G>}&{U6`UuB4RFKLrYO` zzoAG}@K+xTzTQBSWttmGOnNtr=e*Pzmzij!{Al_KaEoWDGK$1>8_jo5D;dD6WST<`YA(GniT}X4{>q z$=JB-vfA9N)KnpYIu)l3O3f5=4C>6>32LfObQ)w3_jo~(mrttQ(n0sq)FQFWpspxd zn_4Q4;`B$d7d={PnV6vv)hcc%>PQ_VIt=Qn+y_%n5T6*-o&LY34i+;yFZjRQCw?KyZpxts1Y(DuZb4zJ6HNw1A)X=bVX9S}E40*^;vuGP7E3elPdi(5 zGPOGOB`xc+h8(ioAWY@DpJlc`r!|NegBoJnq1}F2M#2+m=ZT=fTW#B|J$KoVoX>N* z#e8wP!Tlj85!4KWsR#_tX^X`!gL>544eAwxD))7Zi^N+7HQJX5s;56+b~la~ zpBU6r*@>XOGN_CF-6AA@FsRE_e5ZqY1mGBmm>5<%T&P!!jQ zxZ9vEM_40bi$PtD@LVo-7}N&sr{!4*TXCj&+2AZj|0-UQQM}vHDn2%N&)X~*q5m|f z85y-=rTE^UF31?AU9zGOwZCmeno53ODT)W_wI{!?6oUI6G0mVpLC?EdoMTYmqvu^M&Nrw*$hSt&AJ0;>C#cA{Ml3VP-N<;2 zXfdd_k?|U_#-Nh(yRj|2(V+bKiJ)#Zs0xI3ow&=OPC{tciOmLe2Euo(*ltjZ(Yvk{ zI}K`hd10-%PIMdOl=4yT>%>b2m5rWoy?Dc*3bpsbSyiLlH;69{&a&WJX*Y`R45~G_ z9~3>bqF%ge+$7d9)$MSW{9UVT9-}VL8*N@Mp4N5jtMW|N_2LDCN>)Be`=fZ>po;Ju z_D8Xwsa;}RMz^?GDEQ2S7J<=4pQqg{?qup_`?Sh$(l&_A1~n1XEuxbtef7IdRNt1ThU=95{1?hvOM)N7cc?i5oD>ce!4xKqqAsB=oxr_$QQJcGQbq#M*l24yMT?PwR58kD!x zg6o~j4eEOsy-Tb%sMrjPxJz7bP|YPzrQIzy7}Pr0yj!#xlrP!u=n(fCR7rB&g!B%v z%^E6pT zPTcC)B4!ypi?3EZD9$sebl)iVgW^Jidat}2H%d(g^|=JSUe29LwO`K&=kLA>Ynq^SCaz?qgiMcu@-cU-?9~MnI#Q}R* ztTr?=D}In47S|b6e#O?Yj)%ntgFHR`2YI`=)1YRgZ*^=J_Z!s4k{{$9;vs{&uVkxZ zhj?74Fol-36uPpDYsDktT|=X=c$9X2OM!&xwIyrnDEDI`u1c@Sr75+z!1Wr`H7TRq zkBe-BT8WAB36XD58;ULB2~n<7T#>(u6Af7a%Y@XLmM{ym%T95I!97naY%NrO$gah+ zR-?hQBREfrut7Z=ti=`S6$bTYaFqKgvCg30#(LH%)*IB{v7U8`+YM?^_GojLxW}Ms zvQ5@5vDKhHhRvtNBL?-|Fc^JWJY|q6XsKt!a|RWJGtY=ub&9LhEsiqPEA7%8D>Jfo zl}qATu?(;Bc&?D9Bo7C*nkm}j)T}Io-i0eOsz~oS5sR0me!b^Jra_Txx461`K(<@_ z$)I*=Pp!;=jrUe&sHFFTxO)76-V5SS21T+LMP}`Q>_t&xP$YXvw4FI1dr3TGP$YX< zyghY5_OkfIph)(LsGe@fR8;O2afU&X@Ku4`9)9$SzbfeaNuo%$S2WKYknI)g42op0 ziJn;lve(2v4T@y1i_^{>ki9PE7}TzE96I-jQ|1l`_lX$>MH$}^!xj$6-Vmo46v^Hc zXM_f1Z;E<@BH3G_x@kc6mN>(pNcOf^vh0{l1=(VdBz#9KU4BdmYL!8e>|K!>9+15& z6oVq!dtzN=K=z*4Xiy~EFV?98vi;&#rf3ng2=##2VF)+l7CAzYeDU6;f#)w zvZ3h*#eSWVs!B-n14Fi^Y!;}`3|US7i1hbGc`N6W+VYo(9x=?I$eAATy+KjG?oow( zrLIcCUa{MtNZ1R3B(A~HP8RRth=?nLbke0CNlfq%|0^75OrinjFS9t{|G5t7>$&=& z20OBF<78*zBT0c-{0)RfJP3hRWb#Q0Z;5y|-;|>l#kf|!Iz`%KzzM6ffCH-iI#f(s zMq;AOxg@cI&o#QEG3@A@cnC4!20+);EfanQ__C1U1Mb{X2;ZA^Jg)0X;wmpmsYF~? zqpGr4tg>n%7qIvkj`s!TY(Z=!aT&vYN2xMYOn*Fn8KFzD36GA@F9T!~vtYq4&h~L% z`K?<1ehhliWWVjiFWvf84n5cWs^I?;AG62*%i{6J6Td9}M_h17q6?>YS?mHl9)B0z z4HsA3-EQL^^Ggf=J4gNBRsVm{%=^Xo{*_|3=Uj7=q|zzUC1}b^1C;RM^isYP&y6qP zpi;%KhG8wkGa1fg*uZchpapM-ly?oFSKP?>Er1!=4U-+hec0F#=PSTc;gVj;KS7)+ zy`)k$Uc=V#g1$y3!4J}tm~?NHX|l}}_m@otelsT58|C{kspg5N@)zLp@hMEA?a;?f zCqI^z@MGC>6oC6c5iGqG@Lx&?KHwsJB;$i|wYCE|!R0uhsMtBmrji##ov`9;S|?Pz zx7CT+j=uw@I6e|{IP)CNJdg2tjL%^c^B78~jU*0>AJWpLRl=4s0r5*fmvV-4hcJQmf4!-@| z!unfSzfzi8R44C|S1YsRy>e&y0{Io}Q6{&#lm1G0{H7norB3I@`l?}bf{_bPeKIZIW&VJ_XXU=}+9AeHP z<{V;9J6ycQ)Xpxp!^L|{pRvwotn(S`e8Ze?nDY&Djxy({P^o4|nYB&cRGDcub6c9F z>GphcrL^BO%v>lvmO0wIP~M7j$~Jjoys`vFXwK!T>iiv z0set|l{w9HFmo;A*PCPUMfR;CR-#Uq#CQ_pnNm=NAd`ta7UVJRkS10>YEHsgw*yjw zc?|ug?UpWcq3I3V4|1icDgAl#Vfp#=z2+(t9ksipZohn93PhnCW zDZMXWff%HF?z7y2I3!3l%4yp0F;SMnsE zCf7=jmLIYdBJV#edjP*ykvxdgM4@Sfw_EIy{%%iX_&xX+l?*LrLyOtaenBG!vOi?1wq?PBwQKCJ=TxGjkCtW-1wBpDEv5ezJWz$FBoj zJJq(63)so7>}30rG~fELeG9v?gRS8*z^-~MJJ_REldtlCy-OOF{DHmFlLPq+9= z9*!9(R$QENieo?9JVZLGIAj>5HfxY>K)pXh`z>=Umj>lt4)|ipI>$(GG{-iv2q)fK zOmfmubGvC}`fZNG!d9^faF_K5OifQ0Z*_bnm*i`h?n?5vJ19b(fInsIb{v*>shIUvkm5KgE_-`)?Ck; z>sfOhYmQ{iv8*|kHP^A`I@Vmrnx~;Mcy_=r)*KDeMToHjob1>=LT#~@#lFafXlPB2 zt(0y}rrVbBnTum%MO#i2q%)7~p9HI4wEA$?`o{Qgzr$CU|5AK#FkC#2-hksN53R>B zwD!lpBhzG@#v!dV{W<4QykD;HZ*}}&3R#cDAL3X_=9lsl5=cKe0n<@gPQpUd$=1OM z4s*czCO#X#Df#RKm)Tu`E^vs$dx*n(h{L;u{oTR-?qE-MGN+q4-OO3V#T*ttl~ezp zoU<|UMb_EJI{R2>zf4_hzf6<*Ud)fZiQD9NbG}S$#tuqzRLYa_2&t~(;#YC;tGIYt z(UV*YrO7$jF2A|XI@R?V7q(oQT(Jx|Md&lu|BUqyalxN)!Jl!#RdQLu5ArwC$n;wA zjr3*ORIyO{F!y6u6=L?8>nQV&O4M)*RjInb@A!t>?R|B>l3^k@DIVL#s{pITyGd>_RZQ}_#n}umVi;j~Bg1s~d z6q?wRQXuAvHv(lMMwENY@Q?y7X2L1)S*e^-skV&GvCJAn-DHcjn9MW3x zkAR=W0Tu_CA7oevshl@PqNt1oN6sC`{CbJ%G@tPn#;@SKS1{*FhHcEhoB4M$=N^Wg z%vYab!86Qymf;>0mXX)Pu6)4K519WU!^4mU^MuUxkhvZhUi(rk3?1@XF(l8&(f~^X z%nvdwgmiRXHA}~^bd1V^aSUrA9-mjw;`z*32+rwwEzDUZ-z}_xRk9{J0#~wh4Wt=) z_b|Q*_?*0F*yuCN>1O?I)_;~cd!Vy0uSec2{+#h4a}LWYwaQ@#R^$oOX7Oo;WTIju zlO{3(CKK1sMD;T>-wb|7z``7dX|1?6&xc&4nL*|hf^&ObH8>Y!j^Vsxm_LqTEu{D5 z)kE5m*#J(lf3E35mD-QR3n6|auLa_s%vKYH_X>uqxZqV>@Rh8&2AVJDwL#OD)y_J1 zGrWhTn;`AY>tfC`44-BG9`L`;>p`wrS-s5v08lOWe#qj(5GMqMd9%1Z%VehF&1Q;( z*~~RIb0;;g6~3SkI`3x%SSQH*Lhwt2W0*6JIkn(a2kW7ko!!9vxeOPAUl(iv|D5br z;;7y$Sg;0yi-T>Wi%S7XM`CDV z=&;g6KwjGat$G28>y`1~Hmdl?=ECzvm6TpNZC z+gdR)-_Lj<@X7f#jE}Y5Ev^iVwNYQG1%G~iy=}92+~2^`h2Z=lzm@Sdz^}+}1CIXR zW}`{8odug9SfAf%qjBG9ivhpOMt!1-rF$UVk>AVsVc@Uk3p=~T&|zOI_UHQ;_cJU6 z=hOUZ#%mbXGOTCVKv4BAgwnV9EzD|VxCWezyf(($8E&$Z6Pv*A%kN@Nx1Abm4>-<( z9_I8iJj9v;AD^J@8TuId8CEl_VOY;lZQ!JZVJpKnhV2YH8Fn%3Vc5%1IJp3Z4rdHj zbRXk>hSdyf7}h(fOV%^qz_5imEsVFSoUC$EKW}5aona@#E`~h}dl?>bQdd0${igyV zhMi#OW9W~eIQbdRiy@oUjMp%%XV}27g<)$9_y1K;eJaq#tagT-47(WiFzjV0V!23$ zK8AjV)eLJG)-zlfi@CC(h4D4OU(Ii0d=v1pf=v|1h+8Wz zD)2F02>i-|YQ}4U-&9b~_(I^AY8YPw{I-HkfLjVWnX?D@Qw2SY_cA;jN3l5!{__P= zJeLx`R;ce4_?YlBEClQGf@;QV7}hgvVA#U2m0=shc7~k{yBPK|6bT$BhCYUVhSdqw z8>$(vA*g!mnN`oM28JyRTN$=7Y-iZTu!mtULy^eEF!V9>GpuGk!xRyaFb=9 z%1JfDdWH>d61Onk#;~0^os4%e-ov=?unC5K59O+6yoT|5#v42|JvVr0dTwD(8^cb9 zT^{Uz_NDYN(aYIH64yA1_&&z{Nj#7kuV>i8u$6V%7;k61lkqOr?_s=`ap7f08T!5K zDC0HWD^wIy&qM!CP6KmVnA5_XHbBh(olNvF?1hOvDI$eqkV5OdltM;*_&}%F?_+*7HnA6Uxos4%e z-otn=<073+r<3b`z=?rs=G3Hf*coqSX&dA1%&oeaC63g;N_WvR&Jy5@3S8TaSXk*zwH7Z~Q$ zfU_s1o;mf*X<+>p##>pcwlUGku!~tej7tGB?PpjWpdDfj;|&a3nA6I5JHyTZ72Fxv zEKbkuVoncpdce6ix0gA-JhJ1@Bg-`m>s4m8GSS9(S02}u@m}B)1HK?-t7q5{q|B`h z+nCeEcn{-JKIQf0b20f`48wZnG_bUlVHS}GU0^K>^f0TJGx!R)#sy@ep7DkP zUPuaPhG+#uzhQnm<6V%>4RisA9p?8krfpXQGV- z?TmL7Q2{;7>1B>o%z-WD;)}^p4Z{ZTCkA>MZ!ICdR0^mq-!wb$n&TwrW6nP3B{7F$ zu8rLvyE%SA!g~ov5=XeMab52IhxDQVcXKBKPVf-^RWacW1*ZcZ z@YeyZu+hJOdL)_P&6NZ{C_bAw73Tv!?{5Sg=?Md_^b(xSaFs;`UT41&u+M%C;5OTJ zfV$2&A8DSZ+KIJ-Gi zSC*3#m$IWr+0j!O-_Lk3pDd4I`%BpJ^m4Lu54%#FbQ6W>%HRWl@g;u-{L#X$a4}cs zk+e+t3-L>+n8O^KOG+pnX{E$T%piCJb1IUb0KPtp3OJbbBtdRCJ>WlPbc3Tiol;4q ztVfmbEzMR|%JxyVugXafVkhRO>oFksszNXjB6lF_OIs;aD*HM&T{84=W6F4ov%0#I8Tl_BjztL&%`9h4vrfc zH$84%oEn!9pB#T?{LJ`p{9WB%>!{1;e$u_m{fgV% zLSGnJ2pB)~N9Fc@g8o^p438_B=qRSh^O*2S7yU$M4db(;pYx976DK-v2L83O4j+ z5r&@(&y&Am@Fw;mx^{V&HvGPvFv&DMagyl* z{9cwgT&zO625ASe%}V} z@SG{R@sXVvIc9O_;`yWV#K;NrFP`7nuylLTV{=l>W9KiPzx|3=cAKoHh0dSe(71j4 z>sGV%@-SO{K?S^md+e^QFO462o^C$V-sTbM{>2B#e zx7u>cWSCoqzXNK5A%gSa61lPf#)n7oM&bQsG911L8WHd*y-1wL`c3%ztcCc)rgM=u z#KFLyCyCj}an><9g7+*RD$s;#G;-dB;!+hR$nekD;6^Z#OOQddY>)Pk0y!5pFNKFo zA-(_}k>>;D`hKfPX?XU9)^aP=RjfBEViCp}v4l&0BDvuzn09(PMa6YcJn zm%Of)H}HRlakaEd@`f+n(x!a7Pw1p|Ac0DZleB=iTGpGK(uNL?G!D0Wcykg|$0e2IF3B!GY@KChu_!Tk9{77Ux)u%Mw~*n)X;hgA)pUom*Wu)#wH z4_Z()tg@nV-rV^MHniqTWse>SO8K!vFhFNs-SCl+CY?c3)wTG2aVoxPKT!gv{}4t4 znbw7w(0qCCd;D zzluk*hDujR(-{}V=+h1OI&lFSc|QJ!9}Dn4I~N=p)D6HFV2IB}T7|`XJ|1!hv&LX( v4#xjBYytSg@P%Uqeo1e>Sg_%{6QxR{pweN|dy>3w=%cRT(wEYW6}JBm^4px% delta 30165 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 Qo4Bu4YO#?D96v#JKb2t-6WCgK; z=N^|mKFnb~6I>5BRga?q+*8$WLm3$n`|_O7sk%p7qzo2@N91-cdu$Motl->o+2g}O zr=Lfg=HT&?`=ODs>Y3T@5wI%5v+>^7GB0MwZk?GQ)mw2yM)$RX>ksGkzyVn^tl;Yp zm;Ih6OWY25>Ef~5Tz0j7I1FmeLq&N-;fZ(0>+dKkRGun&JMob*eeK{k3g<_;p(~^z z*6@!Mxa{|)LK=#fHx-^Zn=3Aoio4`dA7T~Q>irw9AV7AM%i3b0rvgxPbGpiHE@D zc4?bI7zc8_9am&*Uml_^RjcAW%$9~&gF|uympwkL`F_Ld(jI`T{Q3vkb7K8NTqM=u zSc_M(i8bh;m7jlW*hq_OD|67tYK{X(Hqm2pS!?3Jc#>_dWLHw%7SxZVT9HMY)*7pr z?f1A{?wW@o)o*!|nTsu*Jvd&sGz@<;yCgai(eb9irE#$L@Dh^#c_S>=C#}n)ZG}Vq zk>XFZcI^=;i>Tss?5h}`ahM7dKsNht7gZJEp;V=6rcdt)l)8B^)E5S8Fg zlxJQ1fhKstepSwo0BbP&e)e@7H-N_@60rg`=hbE#IhNsW>C2L2lDsL)@*Jp@`>fR*%E&$k(v{IR_+!-MF{Mo}S@y z7JIbkxduOSYU3K%-JJHC;iysfN9JE(=upVz%=g#42!^98#*{rRcH{Z!^Zo>heqY5K zyj-C5_v(5g;v-YhF(VP!;4k9gt5@q;%9}fv{f1An5U<};HJo6hvkYb+l?WPoqoXtr80KQ1?k~wXaVDRCJHR?S(xWJ7zZrc>5!=PX9=Di zd*gy&ludc^p2Ma7Q$Sp^m+;tKAY;c0p1fT4yY_KCOx}<5Yj$J?SX+HAHVEB)&CAG9 zNwHebirS^<(D7y=&~W(GpVmg}a7J?0yaT{Fn2mV{cSdj{ZB}jdK{%}01y_+-5ZLMi z%>Nm@+|jC3ADkF2=Kk=@F14N&d}83T#}}>cos7~UoAxUE zw5s(IKs~pL2j#CpSo0*u=+z_hN<4bN{K(tE+rBjIl~m|0yBwN|n)cJQSoxM?i-lptM$y*;I*|iAL+!i8@S@CBG)BzW@~S7 zm)WmT!BEtEeY~kuZXw#&mhK>4wd0YIGNgDvHc5j zl&aXV1?lz$sEL*rNz2Tj<;7eyEEJzA|1w%yW+__cqF>8g<;0f7iPY}jvLTh07shrk zEFC`&w{fAX@mqJ4w_tw^oqc)jl1neFLn`UqRYbqR=!MR~gA(jj$&iI6pHn#zWit4u z3u2=eg@estIrWH%kYMTtjpa-jAWB1-5o=oH=hZ?G5MKOK7QS|T(J6f1&tf?y2W?8- zSYV~o(is0NxZQs&?4ef*SX-N#mSLrBX&nJ89Zh9?QgE-}Qjve@XL+jBy~EFbsAO-p zl@g$$clBLh}c#3pvSAbSRik zFVXe+*>t7yV7is;DeSaVnt4*)o1IIaWYz?&bgjtqr5oSVbMUq>XMK7Ked>v>3#HK0 zo^>HB9iSeImA-{K1wZj*$-QapZb{a2nwyrD`ZBFlp0hXGO`YC%5a^|lTj^?XU7o^y zTAt51Daf_f=P`Y|!uSa26nb{xx=@&2%7~#>F)YK>l@W`Axl@eF1(%A8*+N%%+0mP{ zH#>#)qH;PtgT7knrR*CJ=x)J>lpaeF4GUg@aAQcRndYbZJ7`nr{-Tni z5%hN8ZVW+NCj0yZKCSd}-s6L!Rtlm|=$RxZC49KhO2fT}3!QX6hF}Oa!3884Y;#>6 zhjJ#|rBEYqG>!1ykA@Yb+DgBYaDNNSY+6zHV0t$FJ@3Kv5%itL4G%A6n~2yvQN%-; zJ@}olJBr8I?}W!wR^e@E=1-=**(cMPgAXF=f2WlcSt%RIOrfMf>q4hcttYAER64)# z^IB2DU_PegT59W zuu_>>;RX&Py%-3OB({k0hLjsDxiqY>2913q+Nl!zl-#C*Q)yD+>#;o-4Uf79e;6J| zHcFyaEW`72C-TD`yzg(q56J#rG%<6Wa0+x+WHRT1j=6_mIgT>%`!VJ&g)WlSZ>Jzsg|Ke-Z*aX|3pa}%6KuAHx+yp43(_S6@`1++3xW6M4*-6iHyjUQmgbIw&%5mD7&m|JB*?$Y zJOlUxP@#PP4B$7(=bnrr>aGTcla>NQaqN1wzYeD30u4SZ9V}{sd>P~lJuTSbSPhBQ zc{4CE?zY$q%R8_*)fS);7qPO^R61P>Aye0P4C56#PL9afyw}n)Xa^*VGZ>erFkYC>QO%5d8mPB5J>fafPb6~dEpEnPXiK3= zCtJRfxIT*Fkc$-$ONi;B*pSFZCXAj!Z8DOlxtRV;l67A)Yi}3(!%n8}i)VZ*nbYw* zNyi9jOb?=7ym_S7CQ0IZ37mqjlh*s9$OP0@THsGot@L)@A3<-@6gb*UfBFyd_S_kU zWnbOh$0avsSj5AgpYdR&WKSpLHx>WYhsOh(>6U~(7k$kasgERZ5VrvDRJZlB`4E-o zWPJ*>2HBAw?#dJFzks~AOUDcwD<%0d%~oo(Wkd3`V02xMQzDH|;bu}&`l^LNv+JL$ zc4DpJ{c24F9p{;LylzJ(H`!ivHvCUiIV%^Lc(^yy58S1wk@i_e1x!X-2veJS5pXkI zB4J$#Tw!YL*UakU_M7R-e)8K4xs|T>J&j(T80f}+mQRRfl>66f^lU%%uH%mSC#}~~ z&(^#B>_7S+?7!}3Z}CfZqiFsj>>Rk*X7ND$c+|PyKVSxYXX-y0jmrN+-Tt*cS9%AO zr>EX+%>N{HIU1O2;_3FbX%}!ajR)ST^3+>u=FwVV+6MYN31zi;U}E1aNkl*N$*9k2 zeKO8b{eFeYO_{(~O+ldFd>fYBEoNS#&zUjv=~I?ODFNFK{*=|iB3^R64X-eL0h*ia zYpi#5=u1eUMJm^=H1*Yun!;!-V*XV83`v^O-;EqIXX8pFI~GZ(tU$a&&AILb+4zKOEATsBAT9DsPjUF%y1l z#LH=ub{X91i|VJ#X)|$%e^akO-KZ?BJJpq-xvvVXAIO=QW#i3llhz5kOy!+)n2Gm* z1^#INU5^*u&+U4>@-qW`^Rmi*VdxwjI?gq?r*hY$xhkDYeJs2Lk6>2__Y&>X?(&=H zMT48=S?@5@F@qbL(Lv_erpx{3yjHy4Z=sbz!e^Cze8K(Tt}(c0gPXzKsB`fCj>|%i z3#WT`P$}Pm;hu%PJE_IshQ^Yv$WoeP1Fp!3Hap^}F{FE+H~6TUKsOj%-QZ?N0^O=} z@Q$l5-E*AV;!mXbJn^pVOL@`nrE`T_MRV;h`cr7Lp)1OL*N?}(`MTvl3;yBHqrzKkDV}K} z(>QxRm^zM{4Q^W2j?|OrU4v`&{xP+J^6*BFn}4V10n^EJk-Cfp(okTMXZgcLD)Ke1Z4q=;Ur}yjB*qgr!S3XhPlRAZ#;$bO|cw4bOZ3n58Qi%BS7u)y`~8aH#VboI z=_0zukls*A?u%%R!A(hD?^r@HgPR`9xT-MvQSN%jQu>`C`##q~OX&{=_n>cO+A?~{ z;GXhD!M$#9$$<{Kgx)c@oIo3{&kXL2Uf68cYVO zmg=c^)w15PoSX*tE`AQW91o^+-DGWV!ew}uqI0vfPr&6F+_9_ z1JKpcID;FRl?-l*&S6l?6uO*t8Orwyu1mX|_88ouf;HgYKW?hf74#27=RmgWS7zWe zy1w5WBNW;?~o_7Vs9pFk0uB~i!Ce=-zdkFe z8Fx3Y8Iom~U$4|>6t8zQ(MN{t&o&EI=)Vl^l#EKchQ2kpnHdywH58)vV;a(;9QSop zJVft3$9)|QGq|aP);m_wSc98C$b$W0lEIA&t#@2cQw{E{kcFu**G;t3;4%gkR?^MXVX(snk^5$P)!=?8>Y#P> zmccn=^^vSHa^FIq8j{)hx24@m-x%Ed{0-pv4I=s_SB=|ft#BQVPm1r2?TJi?&dD2Z zzMcN4+t?T7nXI?dD+c#X!KSqJwAbL`@XlyG?HBGP;;Z7XNgKw#JCvNF-=zJT?icRY z_K|~so3??r8eB2BJE=`L{pxo&mErXaUkmp|apAq2rs%A)PkSou9@=AYIDn7&{tn)Z+HRO+KN7ZfAVQ_>+wT<#d>Al#CdA5ztGPwKD);5}BaF3v^ zZL~n==t=FPv?r)e=OkfI&?>|7`$0$5C+KE_vz0babUZ;D40cNTQFS}rZ*bGon;qNf zHwG6eIjZiU#|`d=l4i#adPe84g}&NU==!0klAfgZ4GU*6#cbCUDA-=F$vTJJPg6pf z-jPfFm3WBjGq`L0H8%4aETa4U)}^ehe1IqAsnX{4bG;WE*BOH1JxR`ud~Vh2dX!?2lFfOnDknMdn%vY&(m6idkNRGHoD#5-oW*& zjqWwLK=ycZJ3U}sB6$ZMilh>mmGv?L}IFSJ!x5p`4vE8r&-3_>9wiZ6WO9nlsAS?oSkdQlH(Q zDAV9r_cGm3-luz+?l8EAViz=Lz(;*^MwIPdp&L%=vwMZ^FgVujrp(Gd-EJx|IM%&N zH=Nn0dzJ1mIM%&JZ8Q3GuhAO@$GX=^n`!8x=-lfx%3xXf2GOj(if>S|!Le=+Ej+hR zw}&n_IM%&MFVF7Ny-DvH9P9Sdu=D$LdugJ&Y_dc~49P2(Hb3>o*1M(Ui>-Liwuh4p@(tdIa#}`2hMGsJkq1=i)$xbRYxQ7N+ zf;(M)iqq{#82DLQC(Y0~CB1|#=Nh_;2hIVv(9q?EzfSuLJ!EiA;pNmtj~g5Z(?!c0 z`-16;Qlr7LvYX1A`jp)u@h}6ob}IcIw}=XzoX%SQC^Xr_@;7lqW1^FB|7E5>;1=^l z8@AUay?8nNsPqMH0!?%&ZaxakbRTZ>EOavzR{CDCMMaGc*&2@ctS4 zX@YDt2OjJ+J|N@zOTGNhIrt-n?3aW1X;?oi;TJbQtN4E;$L{g}vw1v;`)TtZ$W+>h zyLXiy0xHy>ei_pZ568p?f3@&`nWyMaoBw|*$@|Ir{+Z^tKfmTACCYb^F5)dO4XEJV z(_>*LUOYaA84LKbs|A+;EqIH_byov@bgR&J0yA(LWC1?rm;eg6?zyRt?ht>@haS)L~(Ld4;8;c zGsud2(-{=So3|M>*YQ`N-|->Mlgjg?@Rx>=P zGA%fJ@?+Z_@|5%uv42GD2P+ecW~jT=3$;1w9<_DQV)Z@srIahx!}LSyY088LMy8h8ZAvG0V7o$5Bl=6?mql5I*=*4t`_*mQ_ZhuBmrw-+CU(HgHQ;*tAP3OUnGY_a!4eHMAC{7?=-bah^+|m0n-t&IU+Vk#O6zpd?}JIMRH6e$0*7@J0_xS z>Wzal&1M-(vogybHV;aDN(A{iKGwse?BlJ3Ryuy<)Ug1gS71}Fwrecjkkts}`DDs4MD8+-H zGJA2ayBS)>Ji(x8yQSS+XnM&tp>^tNca17CA`wJY=~Oc}k{2OtD8SyVP0sCoGkw$HGroW~(a$Pg}OZbr{H= zx5>i(OdS}Asg=qdgAQ2=QTOkbUBJ(yDDKAHM4@SnuY-0ef3+tIehYbJUI#5fVv}jM zdV0ReIvoup(?khqu+rqIq`}a7td)wbugi0b)5BdbYrS@hIC_w*l$xm z&@Q&JeT|iyR}lJF?t82md(_JYZL)H@c0lpFOp|pNrCFb~&QlsmrKqX#AY*bdK<*DS%O$A^0gw_EOj?a-OW<>9;QM$+G#iym?1!cDPH}e_E|7NBnRzifcBpuaeYSe# zpp)%aN&YrtYNy(EN&`D3l%3*VQ5IP@+8>cnc8E7T46sK%mK_pNlSvu;s=Zwqm9p19 z*z{%52ljZ1E7_^;M1_@>4!wd{Zlj{iAM8t%F`5l)eQm(vSfUhZE=N$gBE{#}Ee-4w z`+Z`6r}BuWgFaIe!lNA3%CFF|{TQHWj;j<;?u;mkM@!~A#zM5jv5ii{J?)())qBj` zYN}7a%yAeGe51gZtVgl=Y$|Scu%npDy}!w^P5np4ddFe)uAG}78JlsZqeJ@LA^q-< zejk&Z9fJQ<+Y!Mp1$j#x<%k}W3df|vF{!XmDjbu3m{nd>W|h~-KC#>0+33MV}cP>vTRQ~S1Dxuw!W{tvz-_lN0`R#2yRpQyeM0IHtuWTo({K^(C z@IrUCX=2f<3A;?+X6#K^rn-^#W#~<+?Q(T``X>pOt2Ye#3YPq)b++jqt1GcVGSqB@ z!moRq^*rHHbVef2p?Qh7!^0BCLY2F^Q00ZaQ00aF4CVn&vhT^Vc?Qd7OX7PfZ?tKW z(!r+nIolG0sw>#+IBKe~zL0oGlBt*<3-3>4`-6$tY6c!jTw*%esw6qgA?w@t<@IeT zMM*C67dY}9lEFG8**zrLy+^{`Ea7hEK%?}Ch<1o*hs542%^b#y51#qMauz4=6q^pQ z=@6UUDo?T9DsR|(u=j0EZos){iDR%j4X+lPrQv32xLF$J%k(?ROO#Z$%5vg!QD(sUc_e+!erO7fiso|WRo!qUa%hE0GVAOo0b%5Kfe4vHR zmFjX_5ss<6?U+qG#o|r8{Nhc#{3_J*WN#R2+v4UGzr)Sf3w{Zz(NT9I&WK@8B9+oy z;4r!eIEwxT980Hq6RDi;^}6XCO7^+wJi$u@qY)`?6?{N&n_!lb?6cD?%I97?e#ZF? z@DAk|@E%2hyhX7BH!E?#N5t}RvD^VmCq1qB@cXGcdOA0sV$|#}papbGXduPW=iY(% z_mgi-ABcY#GX@Mr=?N6Y^m`!A#lSI?nK6(iP$BR%8UZ|8)?IL=C?uO|LV)~+Bw>Uj0 z@`EDzo8V!*=D0KT1?Ue03jUeB&%Gvv>zWh}%OdDd*3bi?plCy)4T(Houn^kkqM->2 zr)45U&jlxmqDtXDEfTs>=xe3&wIaDrutnsXM7~KR4+yr2`~{J}Ad(jaccHPjLtPTe zLD3!*dGv2m9EQ3pM5^>dm40Ahp&x<{bq)P16cBAlv>}n_3l>6qG*m9y38I}K@=1b~ z&?ghR(dc)F>x!3DMoMSnoQC?6L-&Sin78C#RVaY zh#aOhG$k*9Y9D9hi=+^ebMwj}31v=@$`eFBNw5;y`FT~)UXWQ0$%lajCeGp_(Jq1Z zvb;uU@6K!z`L%-05>T@#7Uh16>1vp+&TE0`2bry6v`O#*(QbkEp1gLEyde0Z$ag{h zXkHg;`Leo2eo*jlqCE`lpYzDPmCnsFnK>-8SvqW%4x45EJIrh7gS-HY?#c>@Q9cyw z{DqKzoHs!vlSEPp$-nceVEJBFwa6C;E`dBLzY+4Z>?V<1E4Ui+!u%G<=V!NyWRu`# z31TyXSQZU!fzh!1HW;;Jw~P7(!CjC~$nS#uFWKE9IVgA-^0z}|*-F_tibW=@pu>V; z$`3$3FDEFHkYJ&O2c;0&W%=dM-knn+@(F^`iIz>29hzw2gjB-FpH~H=w{xmRy+CjY zA#jFddGvJF|cQfZK^Y@-O81RYl1hyp?f1w(>)*6py&v&Lv; zsE}>)%SB!xI6<(|8eKyV=2r<*Ex15%p>-4eHFqH@AI)8Y%1`Gvio8kiTEW$j=jF8s z-72_Aa0}%Ayf&fR1$RNRE5A$VZo$Klbmfz6X_R|o<32fTYv`N&pwNY&9pMV0C)zeq zd1#^ylO+b>tzD438152Dx8NZ*4G?}(f@}!}1cQR* zf)#>Qg4KeJf=z-gf~|sWg6)FQE-AVN$tev8I-GI1q6dTy3YH622v#|HN>&M7E!ZfM zMxmPoo1Hw*TZC>EY!hr3>=NvbN^!`^bM+7^tPD|{1R)p@490QqgF@%UvD0#)D+H?q zs|6bcn*^I-xiZuubgN*SV7p+KV0T=U8>4t>QZOJG6f7645Udhh5|6zy+$i*F&}+gi zLT>?mINT=mF3?|uyM#UrI=O%n*uNuT4QT}dp$kEm7nCQ+{I3KvwV+BAOF(0*5qdS~ znFU*b7Z$XMWEbd_1zkdS3m#73+#H7dx&kFpnn_$kzb*&}9TY5tJl17wi_KB*~LtKrkp+p2V}ET<8kHDv?wPT`kxs*d*8@*vc67whPlG z*eyuO(u`n0Feq3qSRq&?SS{En*d*8@*ecj2*e=*5*eysd@h=z+ND&k)7p!n`N-Kn} z608<%6l@Y~5o{Ce66{7T{3CI0b{!Bb7pxMjcC)ro=oZ0Nk+emHX&0tTX!3{)!Jvn$ zl?z=Vbd}K69^Rg-J-j_PiljxbO|V@oyM*o*n!M6uFMA6J9rVgN^2+?L5>caIlNhxK z-70jO(Ct#8OXzN)$tQsd27MB!&=o>g30*C8qtH!4w+gn2q}_+}PqV*Em~LN;EFnq} zr-H#0KC1*%Vw4;z7fD45H&c}&n~zAUMbapeMv=4#wh49#cEdxnpZt;oKVR+@Kl=>e zS2xiQ0|8N#gTWjYx&lV+xfL)P8mbaWwVxYq6q`noG>N1|BrPIo6YLUHQn|@MDmNKS zMq!$m@wW)wDs-FB?Lv16-3=GWP@423jqL+M2Zb&dx=OH7 zuqlmaNmClnk`|G)ie;P7?LtSpgy|NB(#3T;#~uV)LggZ<5J{ELP3aP-(5*tZ3EeLC zT|!fa_{rc4w~`^L7D+IJ>y`^$CD<61q6tR0R0!RQYT2Pysn#Zv4yo29bhprlpuH|c znbJ(AWJ2h2!HP^7zDyZDku(Z6iM&bVEh1^njB@2RVcNwJWwFzMU@(irs>tGiDnJhn zHGyu=Z4$agY+AF#gV60lcV)doF9ZYG>?fGb9?FHT$QD14U{;BwFHxcL z6uL?1Hle!&y93;VZphCHQBd3k#huVWq05D?608o2H=&zE+ah$U&}~d(|A*1rxm}{@ zmI{YjbvP0ESz>&~RLbr&v9g_Ckc9C?6q`N?Rqlr6>wLwkh zwn(Zq);0;Yh@@5McGy@#T_On-*7NzlqL59i3fWOpp(IV{Ho-2zZn05{q{$*_Qs{D_ zD}=5Ry1IxxGz#4$@)n_6AxBzuDR$H#4pn-jk!ensNYq~9bxliZU$)m7ns)cvfxE%xfx)1vsv3SLbAKGy=A>|y%N zVx~_na034jOaiX7d4XT2FkU&Bv7uIGSpo5pC)XYD%~jE{?bTq(cr0~JJZ+u85@c@+#!;`i#Uj3Mqg9J-3R$>v`t+0OaSdz=qDlj5@Cu8F%P?qHmf;7G_z z7@Tl=!v2JV3E7F^#3_k2iPcF0fmoemwMJ**jsD^sn3LUMGJRbYk7> z<*#}iQeTIExmO!M`-=8&q~>QDj%j+8{&rXVOe0sLZ$A#^urzXF!`nr6Jm_Rp>U7`V z=P-8CQyIk{xhKh=1{^mNtB8csU-psv_1JY!MGFsy6j+$)E%9Tet-@O=Cr^dkO2(lhlz;zm;h<$ZD6)P(qLYC-%YbtuXy_)f=n6~4FQ+YR^)7s$!PlJlmO3)g zV!Ajn$FvvUFA@u)rmw9zCT~)XDVQ`}ITzo{@Vy$}yOSzR!Q=|l5PZv%N7FQvXQR9n z<+|kQraHC4^ht8HsnE5+G!Wm3uIb9XuA5AW?&-=X_cux%zITJ(jqh7-^cWlJgk?3C zE*hUlV<#`VbWv^f<%_0nw?8|_zkSJ`7fsvC-cro5liypreeZjFT-zV`txK{_KfBMazlV<$zRU0rC@sT3*Vg-C3O^zwURFn;J33^;VFXkO6k~;I9gem zWR`aAn4Zz=;%C*WYp{W?Jf=1$1)_nXdC*r4Vw#n^eJOR&VMA)9jnU{LT8Cx7rJwx7?$5_qV)bkMV*7QrjHDHh=jPrfhGj zXP{@mz+lYC0Av6;+DVC)X{o8EX%@+eX^DvzX(`DTmL{nNCTSKXrY1&dmKKHvhAGLZ zX$%a`j0|?u9VHkww{H?-bmM0cDx7y|`X5!s00F*PH9XDc=e&15oAUC@jI}}2W7HU@ zD4>gi)L(gByFq=cpwR0-_or+ZP-m=Q@s~yOCZk> YC~gQ=m&%YfJz9s+5Ngpx9mafS0K8jn7ytkO delta 441 zcmZp8z|!!5WkLtbx`X?lZ|o7;$jrjPF?r!eWiWkwqpCKL&j152r6DY2u_KGBijtGd40YO0`T&GBHXtO*S*LFfvIq zwJ=FxU~pz+u$%5E!Kk@?lNh5LKTGoVcg@rPs4@l!n0Tf3GcK}qn9?P;KzPn%lj$*P zj8hb#q6{!mkotQ!`8qq+s8^*mM%Hf^P-m=Qo6KZEt;sqn9mFVeTZd# diff --git a/jackify/engine/Wabbajack.Paths.IO.dll b/jackify/engine/Wabbajack.Paths.IO.dll index e15b759159e97e65c42c3db0531165c03bac537c..74be9d35c3be4a5bdc66a0c393189ad18a11be98 100644 GIT binary patch delta 337 zcmZpez|=5-X+j4}lp^n&jXlqTm{}PZCYJ^)gXqn(gLP#DZag*l=H++6dvC*oWu+Z+ z&Tqbve~Zh~RL?-qfPul7kpaj6aLxVIkVcRt%jTsv-KDZCI>c{Y-Vix&jW{YPZQrb5lflFfbRZZ&>CMyaJQ-O{8T1$oHb1r3XAB238MGOa7!nyQ8PXV1 zfzT94TQDR8Mbdyc5y(zsNCEOKfGSLY@&-T>q{0NK(uBbX$hHLX41wZ?V0EbsX_K=Z M4WZU7bc|;L05PReLjV8( delta 313 zcmZqZU~K4MoY2AYAWz`q#-2%P%q$EXn-$gB`2-%F>)&)<@UZ798LJy>1&=#ywlK|S zwKUPQ&@*6QFlJ=XGtdKaw2jRTk_?kf4J}jB(u@oZ(#(vFOpH=3(~?Y#5>1oM%q)yd z(o8K(QYJsJieO<{s?|2x#yUV?(aIt%Ru(7cRl(+wb46p{PM%|(q5u^Pegzc-s%Fay zxToSgFY)2h%GsTp6>Kt?_*oc0fP(=_Z=P=F$;fKLpvPdb`Ki4=V>p<}pv_>+V9sE` zki=jJ#HK*fk|Bj54G4{ZYy%)|22^3hU;@OcK)E!aDiAvns5TiWXU1RwmQ4flEr2p9 Pld~NSq1G&PjAsG>UY}3D diff --git a/jackify/engine/Wabbajack.RateLimiter.dll b/jackify/engine/Wabbajack.RateLimiter.dll index 32a3cd9920b7966e6c34361eb857d2ed7783db1a..25c7185d5dd89bf7dc1001a4f33183fbd3f8f952 100644 GIT binary patch delta 324 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 diff --git a/jackify/engine/Wabbajack.VFS.Interfaces.dll b/jackify/engine/Wabbajack.VFS.Interfaces.dll index 327e2bd2a04de74423a7e9a0b6ee9dc54932ee9f..8c077ffe01f38bbd90fc05089342e5e2e96f4d18 100644 GIT binary patch delta 350 zcmZqBXwaC@!Q#AB>(a)aU(C#`3=ESyS(L?rG#t1wL6|_X&4*cJnFUyEdT&VOSb8md zw}~~&G3f8)5A4q@P4x`)3>X-U85w{KAV)hX(K0PH)ili_IWa9U(IPD+*}~E!)xadp z!o<|XD9zHs(7-SyIW=uEKc^x~wRz{m$tIiu0y7F<&p8rl=shcjYisW_;jqb5ILj2E zg4|J1L7?iy%p>>Pu1h@nQSx@0!DcP4I3|9Wo1ygP={$vutfmZl38++c&W@cqzm|Q(aSsO^hK^{AVi7b{Z0~ecoa*nDvvfNQA zgdFQ!R5=N0gq-7COR#;lbA3Vd=CgD44F!58aXRhUvCO^m1rO_tF2=>1dCqIdTbk+_ z=ov6D7&9^e89;c_^8r*4sM=Zhb8nmP zCFk<=i%btUhkP|*f9QRhlpu0oj&7o*_`&5UehhA#HN_PeZ6R Iy+4bY0L(RM>Hq)$ delta 434 zcmZqp!QAkJc|r$E-R?zCH}<@l&CJ5UF}ZqKh8gOi^c<&d=b`nk4X7vwQ>7W}fpJ@|Grg z7J3E@491KMdIowxj<&J6L6TvTsi9>`TAGoeL7JJdk%>{NWm=MnQKD(GnVE%=Nt&sJ zNy_Hjcg$=oA2vkao?P-JKp^7UTw~rXvpwdW=E>B{yu&m3%9jKMsGx-$R1l8x^5rYX3rvl~DfT}?3M4;Mapqv?l1z0u>$hQE>q)ZO~ MX$ZBZ_h&H^07(C3ng9R* diff --git a/jackify/engine/jackify-engine.deps.json b/jackify/engine/jackify-engine.deps.json index 812caa4..4c3c478 100644 --- a/jackify/engine/jackify-engine.deps.json +++ b/jackify/engine/jackify-engine.deps.json @@ -7,7 +7,7 @@ "targets": { ".NETCoreApp,Version=v8.0": {}, ".NETCoreApp,Version=v8.0/linux-x64": { - "jackify-engine/0.4.8": { + "jackify-engine/0.5.0": { "dependencies": { "Markdig": "0.40.0", "Microsoft.Extensions.Configuration.Json": "9.0.1", @@ -22,16 +22,16 @@ "SixLabors.ImageSharp": "3.1.6", "System.CommandLine": "2.0.0-beta4.22272.1", "System.CommandLine.NamingConventionBinder": "2.0.0-beta4.22272.1", - "Wabbajack.CLI.Builder": "0.4.8", - "Wabbajack.Downloaders.Bethesda": "0.4.8", - "Wabbajack.Downloaders.Dispatcher": "0.4.8", - "Wabbajack.Hashing.xxHash64": "0.4.8", - "Wabbajack.Networking.Discord": "0.4.8", - "Wabbajack.Networking.GitHub": "0.4.8", - "Wabbajack.Paths.IO": "0.4.8", - "Wabbajack.Server.Lib": "0.4.8", - "Wabbajack.Services.OSIntegrated": "0.4.8", - "Wabbajack.VFS": "0.4.8", + "Wabbajack.CLI.Builder": "0.5.0", + "Wabbajack.Downloaders.Bethesda": "0.5.0", + "Wabbajack.Downloaders.Dispatcher": "0.5.0", + "Wabbajack.Hashing.xxHash64": "0.5.0", + "Wabbajack.Networking.Discord": "0.5.0", + "Wabbajack.Networking.GitHub": "0.5.0", + "Wabbajack.Paths.IO": "0.5.0", + "Wabbajack.Server.Lib": "0.5.0", + "Wabbajack.Services.OSIntegrated": "0.5.0", + "Wabbajack.VFS": "0.5.0", "MegaApiClient": "1.0.0.0", "runtimepack.Microsoft.NETCore.App.Runtime.linux-x64": "8.0.23" }, @@ -1781,7 +1781,7 @@ } } }, - "Wabbajack.CLI.Builder/0.4.8": { + "Wabbajack.CLI.Builder/0.5.0": { "dependencies": { "Microsoft.Extensions.Configuration.Json": "9.0.1", "Microsoft.Extensions.DependencyInjection": "9.0.1", @@ -1791,109 +1791,109 @@ "Microsoft.Extensions.Logging.Abstractions": "9.0.1", "System.CommandLine": "2.0.0-beta4.22272.1", "System.CommandLine.NamingConventionBinder": "2.0.0-beta4.22272.1", - "Wabbajack.Paths": "0.4.8" + "Wabbajack.Paths": "0.5.0" }, "runtime": { "Wabbajack.CLI.Builder.dll": {} } }, - "Wabbajack.Common/0.4.8": { + "Wabbajack.Common/0.5.0": { "dependencies": { "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1", "System.Reactive": "6.0.1", - "Wabbajack.DTOs": "0.4.8", - "Wabbajack.Networking.Http": "0.4.8", - "Wabbajack.Paths.IO": "0.4.8" + "Wabbajack.DTOs": "0.5.0", + "Wabbajack.Networking.Http": "0.5.0", + "Wabbajack.Paths.IO": "0.5.0" }, "runtime": { "Wabbajack.Common.dll": {} } }, - "Wabbajack.Compiler/0.4.8": { + "Wabbajack.Compiler/0.5.0": { "dependencies": { "F23.StringSimilarity": "6.0.0", "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Newtonsoft.Json": "13.0.3", "SixLabors.ImageSharp": "3.1.6", - "Wabbajack.Downloaders.Dispatcher": "0.4.8", - "Wabbajack.Installer": "0.4.8", - "Wabbajack.VFS": "0.4.8", + "Wabbajack.Downloaders.Dispatcher": "0.5.0", + "Wabbajack.Installer": "0.5.0", + "Wabbajack.VFS": "0.5.0", "ini-parser-netstandard": "2.5.2" }, "runtime": { "Wabbajack.Compiler.dll": {} } }, - "Wabbajack.Compression.BSA/0.4.8": { + "Wabbajack.Compression.BSA/0.5.0": { "dependencies": { "K4os.Compression.LZ4.Streams": "1.3.8", "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "SharpZipLib": "1.4.2", - "Wabbajack.Common": "0.4.8", - "Wabbajack.DTOs": "0.4.8" + "Wabbajack.Common": "0.5.0", + "Wabbajack.DTOs": "0.5.0" }, "runtime": { "Wabbajack.Compression.BSA.dll": {} } }, - "Wabbajack.Compression.Zip/0.4.8": { + "Wabbajack.Compression.Zip/0.5.0": { "dependencies": { - "Wabbajack.IO.Async": "0.4.8" + "Wabbajack.IO.Async": "0.5.0" }, "runtime": { "Wabbajack.Compression.Zip.dll": {} } }, - "Wabbajack.Configuration/0.4.8": { + "Wabbajack.Configuration/0.5.0": { "runtime": { "Wabbajack.Configuration.dll": {} } }, - "Wabbajack.Downloaders.Bethesda/0.4.8": { + "Wabbajack.Downloaders.Bethesda/0.5.0": { "dependencies": { "LibAES-CTR": "1.1.0", "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "SharpZipLib": "1.4.2", - "Wabbajack.Common": "0.4.8", - "Wabbajack.Downloaders.Interfaces": "0.4.8", - "Wabbajack.Networking.BethesdaNet": "0.4.8" + "Wabbajack.Common": "0.5.0", + "Wabbajack.Downloaders.Interfaces": "0.5.0", + "Wabbajack.Networking.BethesdaNet": "0.5.0" }, "runtime": { "Wabbajack.Downloaders.Bethesda.dll": {} } }, - "Wabbajack.Downloaders.Dispatcher/0.4.8": { + "Wabbajack.Downloaders.Dispatcher/0.5.0": { "dependencies": { "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1", "Newtonsoft.Json": "13.0.3", "SixLabors.ImageSharp": "3.1.6", - "Wabbajack.Downloaders.Bethesda": "0.4.8", - "Wabbajack.Downloaders.GameFile": "0.4.8", - "Wabbajack.Downloaders.GoogleDrive": "0.4.8", - "Wabbajack.Downloaders.Http": "0.4.8", - "Wabbajack.Downloaders.IPS4OAuth2Downloader": "0.4.8", - "Wabbajack.Downloaders.Interfaces": "0.4.8", - "Wabbajack.Downloaders.Manual": "0.4.8", - "Wabbajack.Downloaders.MediaFire": "0.4.8", - "Wabbajack.Downloaders.Mega": "0.4.8", - "Wabbajack.Downloaders.ModDB": "0.4.8", - "Wabbajack.Downloaders.Nexus": "0.4.8", - "Wabbajack.Downloaders.VerificationCache": "0.4.8", - "Wabbajack.Downloaders.WabbajackCDN": "0.4.8", - "Wabbajack.Networking.WabbajackClientApi": "0.4.8" + "Wabbajack.Downloaders.Bethesda": "0.5.0", + "Wabbajack.Downloaders.GameFile": "0.5.0", + "Wabbajack.Downloaders.GoogleDrive": "0.5.0", + "Wabbajack.Downloaders.Http": "0.5.0", + "Wabbajack.Downloaders.IPS4OAuth2Downloader": "0.5.0", + "Wabbajack.Downloaders.Interfaces": "0.5.0", + "Wabbajack.Downloaders.Manual": "0.5.0", + "Wabbajack.Downloaders.MediaFire": "0.5.0", + "Wabbajack.Downloaders.Mega": "0.5.0", + "Wabbajack.Downloaders.ModDB": "0.5.0", + "Wabbajack.Downloaders.Nexus": "0.5.0", + "Wabbajack.Downloaders.VerificationCache": "0.5.0", + "Wabbajack.Downloaders.WabbajackCDN": "0.5.0", + "Wabbajack.Networking.WabbajackClientApi": "0.5.0" }, "runtime": { "Wabbajack.Downloaders.Dispatcher.dll": {} } }, - "Wabbajack.Downloaders.GameFile/0.4.8": { + "Wabbajack.Downloaders.GameFile/0.5.0": { "dependencies": { "GameFinder.StoreHandlers.EADesktop": "4.5.0", "GameFinder.StoreHandlers.EGS": "4.5.0", @@ -1903,360 +1903,361 @@ "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "SixLabors.ImageSharp": "3.1.6", - "Wabbajack.Downloaders.Interfaces": "0.4.8", - "Wabbajack.VFS": "0.4.8" + "Wabbajack.Downloaders.Interfaces": "0.5.0", + "Wabbajack.VFS": "0.5.0" }, "runtime": { "Wabbajack.Downloaders.GameFile.dll": {} } }, - "Wabbajack.Downloaders.GoogleDrive/0.4.8": { + "Wabbajack.Downloaders.GoogleDrive/0.5.0": { "dependencies": { "HtmlAgilityPack": "1.11.72", "Microsoft.AspNetCore.Http.Extensions": "2.3.0", "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1", - "Wabbajack.Common": "0.4.8", - "Wabbajack.DTOs": "0.4.8", - "Wabbajack.Downloaders.Interfaces": "0.4.8", - "Wabbajack.Networking.Http": "0.4.8", - "Wabbajack.Networking.Http.Interfaces": "0.4.8" + "Wabbajack.Common": "0.5.0", + "Wabbajack.DTOs": "0.5.0", + "Wabbajack.Downloaders.Interfaces": "0.5.0", + "Wabbajack.Networking.Http": "0.5.0", + "Wabbajack.Networking.Http.Interfaces": "0.5.0" }, "runtime": { "Wabbajack.Downloaders.GoogleDrive.dll": {} } }, - "Wabbajack.Downloaders.Http/0.4.8": { + "Wabbajack.Downloaders.Http/0.5.0": { "dependencies": { "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1", - "Wabbajack.Common": "0.4.8", - "Wabbajack.DTOs": "0.4.8", - "Wabbajack.Downloaders.Interfaces": "0.4.8", - "Wabbajack.Networking.BethesdaNet": "0.4.8", - "Wabbajack.Networking.Http.Interfaces": "0.4.8", - "Wabbajack.Paths.IO": "0.4.8" + "Wabbajack.Common": "0.5.0", + "Wabbajack.DTOs": "0.5.0", + "Wabbajack.Downloaders.Interfaces": "0.5.0", + "Wabbajack.Networking.BethesdaNet": "0.5.0", + "Wabbajack.Networking.Http.Interfaces": "0.5.0", + "Wabbajack.Paths.IO": "0.5.0" }, "runtime": { "Wabbajack.Downloaders.Http.dll": {} } }, - "Wabbajack.Downloaders.Interfaces/0.4.8": { + "Wabbajack.Downloaders.Interfaces/0.5.0": { "dependencies": { "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", - "Wabbajack.Compression.Zip": "0.4.8", - "Wabbajack.DTOs": "0.4.8", - "Wabbajack.Paths.IO": "0.4.8" + "Wabbajack.Compression.Zip": "0.5.0", + "Wabbajack.DTOs": "0.5.0", + "Wabbajack.Paths.IO": "0.5.0" }, "runtime": { "Wabbajack.Downloaders.Interfaces.dll": {} } }, - "Wabbajack.Downloaders.IPS4OAuth2Downloader/0.4.8": { + "Wabbajack.Downloaders.IPS4OAuth2Downloader/0.5.0": { "dependencies": { "F23.StringSimilarity": "6.0.0", "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1", - "Wabbajack.Common": "0.4.8", - "Wabbajack.Downloaders.Interfaces": "0.4.8", - "Wabbajack.Networking.Http": "0.4.8", - "Wabbajack.Networking.Http.Interfaces": "0.4.8" + "Wabbajack.Common": "0.5.0", + "Wabbajack.Downloaders.Interfaces": "0.5.0", + "Wabbajack.Networking.Http": "0.5.0", + "Wabbajack.Networking.Http.Interfaces": "0.5.0" }, "runtime": { "Wabbajack.Downloaders.IPS4OAuth2Downloader.dll": {} } }, - "Wabbajack.Downloaders.Manual/0.4.8": { + "Wabbajack.Downloaders.Manual/0.5.0": { "dependencies": { "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1", - "Wabbajack.Common": "0.4.8", - "Wabbajack.Downloaders.Interfaces": "0.4.8" + "Wabbajack.Common": "0.5.0", + "Wabbajack.Downloaders.Interfaces": "0.5.0" }, "runtime": { "Wabbajack.Downloaders.Manual.dll": {} } }, - "Wabbajack.Downloaders.MediaFire/0.4.8": { + "Wabbajack.Downloaders.MediaFire/0.5.0": { "dependencies": { "HtmlAgilityPack": "1.11.72", "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1", - "Wabbajack.Common": "0.4.8", - "Wabbajack.Downloaders.Interfaces": "0.4.8", - "Wabbajack.Networking.Http.Interfaces": "0.4.8" + "Wabbajack.Common": "0.5.0", + "Wabbajack.Downloaders.Interfaces": "0.5.0", + "Wabbajack.Networking.Http.Interfaces": "0.5.0" }, "runtime": { "Wabbajack.Downloaders.MediaFire.dll": {} } }, - "Wabbajack.Downloaders.Mega/0.4.8": { + "Wabbajack.Downloaders.Mega/0.5.0": { "dependencies": { "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1", "Newtonsoft.Json": "13.0.3", - "Wabbajack.Common": "0.4.8", - "Wabbajack.Downloaders.Interfaces": "0.4.8", - "Wabbajack.Paths.IO": "0.4.8" + "Wabbajack.Common": "0.5.0", + "Wabbajack.Downloaders.Interfaces": "0.5.0", + "Wabbajack.Paths.IO": "0.5.0" }, "runtime": { "Wabbajack.Downloaders.Mega.dll": {} } }, - "Wabbajack.Downloaders.ModDB/0.4.8": { + "Wabbajack.Downloaders.ModDB/0.5.0": { "dependencies": { "HtmlAgilityPack": "1.11.72", "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1", "Newtonsoft.Json": "13.0.3", - "Wabbajack.Common": "0.4.8", - "Wabbajack.Downloaders.Interfaces": "0.4.8", - "Wabbajack.Networking.Http": "0.4.8", - "Wabbajack.Networking.Http.Interfaces": "0.4.8" + "Wabbajack.Common": "0.5.0", + "Wabbajack.Downloaders.Interfaces": "0.5.0", + "Wabbajack.Networking.Http": "0.5.0", + "Wabbajack.Networking.Http.Interfaces": "0.5.0" }, "runtime": { "Wabbajack.Downloaders.ModDB.dll": {} } }, - "Wabbajack.Downloaders.Nexus/0.4.8": { + "Wabbajack.Downloaders.Nexus/0.5.0": { "dependencies": { "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", - "Wabbajack.DTOs": "0.4.8", - "Wabbajack.Downloaders.Interfaces": "0.4.8", - "Wabbajack.Hashing.xxHash64": "0.4.8", - "Wabbajack.Networking.Http": "0.4.8", - "Wabbajack.Networking.Http.Interfaces": "0.4.8", - "Wabbajack.Networking.NexusApi": "0.4.8", - "Wabbajack.Paths": "0.4.8" + "Wabbajack.DTOs": "0.5.0", + "Wabbajack.Downloaders.Interfaces": "0.5.0", + "Wabbajack.Hashing.xxHash64": "0.5.0", + "Wabbajack.Networking.Http": "0.5.0", + "Wabbajack.Networking.Http.Interfaces": "0.5.0", + "Wabbajack.Networking.NexusApi": "0.5.0", + "Wabbajack.Paths": "0.5.0" }, "runtime": { "Wabbajack.Downloaders.Nexus.dll": {} } }, - "Wabbajack.Downloaders.VerificationCache/0.4.8": { + "Wabbajack.Downloaders.VerificationCache/0.5.0": { "dependencies": { "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1", "Stub.System.Data.SQLite.Core.NetStandard": "1.0.119", - "Wabbajack.DTOs": "0.4.8", - "Wabbajack.Paths.IO": "0.4.8" + "Wabbajack.DTOs": "0.5.0", + "Wabbajack.Paths.IO": "0.5.0" }, "runtime": { "Wabbajack.Downloaders.VerificationCache.dll": {} } }, - "Wabbajack.Downloaders.WabbajackCDN/0.4.8": { + "Wabbajack.Downloaders.WabbajackCDN/0.5.0": { "dependencies": { "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1", "Microsoft.Toolkit.HighPerformance": "7.1.2", - "Wabbajack.Common": "0.4.8", - "Wabbajack.Downloaders.Interfaces": "0.4.8", - "Wabbajack.Networking.Http": "0.4.8", - "Wabbajack.RateLimiter": "0.4.8" + "Wabbajack.Common": "0.5.0", + "Wabbajack.Downloaders.Interfaces": "0.5.0", + "Wabbajack.Networking.Http": "0.5.0", + "Wabbajack.RateLimiter": "0.5.0" }, "runtime": { "Wabbajack.Downloaders.WabbajackCDN.dll": {} } }, - "Wabbajack.DTOs/0.4.8": { + "Wabbajack.DTOs/0.5.0": { "dependencies": { "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", - "Wabbajack.Hashing.xxHash64": "0.4.8", - "Wabbajack.Paths": "0.4.8" + "Wabbajack.Hashing.xxHash64": "0.5.0", + "Wabbajack.Paths": "0.5.0" }, "runtime": { "Wabbajack.DTOs.dll": {} } }, - "Wabbajack.FileExtractor/0.4.8": { + "Wabbajack.FileExtractor/0.5.0": { "dependencies": { "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1", "OMODFramework": "3.0.1", - "Wabbajack.Common": "0.4.8", - "Wabbajack.Compression.BSA": "0.4.8", - "Wabbajack.Hashing.PHash": "0.4.8", - "Wabbajack.Paths": "0.4.8" + "Wabbajack.Common": "0.5.0", + "Wabbajack.Compression.BSA": "0.5.0", + "Wabbajack.Hashing.PHash": "0.5.0", + "Wabbajack.Paths": "0.5.0" }, "runtime": { "Wabbajack.FileExtractor.dll": {} } }, - "Wabbajack.Hashing.PHash/0.4.8": { + "Wabbajack.Hashing.PHash/0.5.0": { "dependencies": { "BCnEncoder.Net.ImageSharp": "1.1.1", "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Shipwreck.Phash": "0.5.0", "SixLabors.ImageSharp": "3.1.6", - "Wabbajack.Common": "0.4.8", - "Wabbajack.DTOs": "0.4.8", - "Wabbajack.Paths": "0.4.8", - "Wabbajack.Paths.IO": "0.4.8" + "Wabbajack.Common": "0.5.0", + "Wabbajack.DTOs": "0.5.0", + "Wabbajack.Paths": "0.5.0", + "Wabbajack.Paths.IO": "0.5.0" }, "runtime": { "Wabbajack.Hashing.PHash.dll": {} } }, - "Wabbajack.Hashing.xxHash64/0.4.8": { + "Wabbajack.Hashing.xxHash64/0.5.0": { "dependencies": { - "Wabbajack.Paths": "0.4.8", - "Wabbajack.RateLimiter": "0.4.8" + "Wabbajack.Paths": "0.5.0", + "Wabbajack.RateLimiter": "0.5.0" }, "runtime": { "Wabbajack.Hashing.xxHash64.dll": {} } }, - "Wabbajack.Installer/0.4.8": { + "Wabbajack.Installer/0.5.0": { "dependencies": { "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Newtonsoft.Json": "13.0.3", "Octopus.Octodiff": "2.0.548", "SixLabors.ImageSharp": "3.1.6", - "Wabbajack.DTOs": "0.4.8", - "Wabbajack.Downloaders.Dispatcher": "0.4.8", - "Wabbajack.Downloaders.GameFile": "0.4.8", - "Wabbajack.FileExtractor": "0.4.8", - "Wabbajack.Networking.WabbajackClientApi": "0.4.8", - "Wabbajack.Paths": "0.4.8", - "Wabbajack.Paths.IO": "0.4.8", - "Wabbajack.VFS": "0.4.8", + "Wabbajack.DTOs": "0.5.0", + "Wabbajack.Downloaders.Dispatcher": "0.5.0", + "Wabbajack.Downloaders.GameFile": "0.5.0", + "Wabbajack.FileExtractor": "0.5.0", + "Wabbajack.Networking.NexusApi": "0.5.0", + "Wabbajack.Networking.WabbajackClientApi": "0.5.0", + "Wabbajack.Paths": "0.5.0", + "Wabbajack.Paths.IO": "0.5.0", + "Wabbajack.VFS": "0.5.0", "ini-parser-netstandard": "2.5.2" }, "runtime": { "Wabbajack.Installer.dll": {} } }, - "Wabbajack.IO.Async/0.4.8": { + "Wabbajack.IO.Async/0.5.0": { "runtime": { "Wabbajack.IO.Async.dll": {} } }, - "Wabbajack.Networking.BethesdaNet/0.4.8": { + "Wabbajack.Networking.BethesdaNet/0.5.0": { "dependencies": { "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", - "Wabbajack.DTOs": "0.4.8", - "Wabbajack.Networking.Http": "0.4.8", - "Wabbajack.Networking.Http.Interfaces": "0.4.8" + "Wabbajack.DTOs": "0.5.0", + "Wabbajack.Networking.Http": "0.5.0", + "Wabbajack.Networking.Http.Interfaces": "0.5.0" }, "runtime": { "Wabbajack.Networking.BethesdaNet.dll": {} } }, - "Wabbajack.Networking.Discord/0.4.8": { + "Wabbajack.Networking.Discord/0.5.0": { "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1", - "Wabbajack.Networking.Http.Interfaces": "0.4.8" + "Wabbajack.Networking.Http.Interfaces": "0.5.0" }, "runtime": { "Wabbajack.Networking.Discord.dll": {} } }, - "Wabbajack.Networking.GitHub/0.4.8": { + "Wabbajack.Networking.GitHub/0.5.0": { "dependencies": { "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1", "Octokit": "14.0.0", - "Wabbajack.DTOs": "0.4.8", - "Wabbajack.Networking.Http.Interfaces": "0.4.8" + "Wabbajack.DTOs": "0.5.0", + "Wabbajack.Networking.Http.Interfaces": "0.5.0" }, "runtime": { "Wabbajack.Networking.GitHub.dll": {} } }, - "Wabbajack.Networking.Http/0.4.8": { + "Wabbajack.Networking.Http/0.5.0": { "dependencies": { "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.Http": "9.0.1", "Microsoft.Extensions.Logging": "9.0.1", - "Wabbajack.Configuration": "0.4.8", - "Wabbajack.Downloaders.Interfaces": "0.4.8", - "Wabbajack.Hashing.xxHash64": "0.4.8", - "Wabbajack.Networking.Http.Interfaces": "0.4.8", - "Wabbajack.Paths": "0.4.8", - "Wabbajack.Paths.IO": "0.4.8" + "Wabbajack.Configuration": "0.5.0", + "Wabbajack.Downloaders.Interfaces": "0.5.0", + "Wabbajack.Hashing.xxHash64": "0.5.0", + "Wabbajack.Networking.Http.Interfaces": "0.5.0", + "Wabbajack.Paths": "0.5.0", + "Wabbajack.Paths.IO": "0.5.0" }, "runtime": { "Wabbajack.Networking.Http.dll": {} } }, - "Wabbajack.Networking.Http.Interfaces/0.4.8": { + "Wabbajack.Networking.Http.Interfaces/0.5.0": { "dependencies": { - "Wabbajack.Hashing.xxHash64": "0.4.8" + "Wabbajack.Hashing.xxHash64": "0.5.0" }, "runtime": { "Wabbajack.Networking.Http.Interfaces.dll": {} } }, - "Wabbajack.Networking.NexusApi/0.4.8": { + "Wabbajack.Networking.NexusApi/0.5.0": { "dependencies": { "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1", - "Wabbajack.DTOs": "0.4.8", - "Wabbajack.Networking.Http": "0.4.8", - "Wabbajack.Networking.Http.Interfaces": "0.4.8", - "Wabbajack.Networking.WabbajackClientApi": "0.4.8" + "Wabbajack.DTOs": "0.5.0", + "Wabbajack.Networking.Http": "0.5.0", + "Wabbajack.Networking.Http.Interfaces": "0.5.0", + "Wabbajack.Networking.WabbajackClientApi": "0.5.0" }, "runtime": { "Wabbajack.Networking.NexusApi.dll": {} } }, - "Wabbajack.Networking.WabbajackClientApi/0.4.8": { + "Wabbajack.Networking.WabbajackClientApi/0.5.0": { "dependencies": { "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1", "Octokit": "14.0.0", - "Wabbajack.Common": "0.4.8", - "Wabbajack.DTOs": "0.4.8", - "Wabbajack.Paths.IO": "0.4.8", - "Wabbajack.VFS.Interfaces": "0.4.8", + "Wabbajack.Common": "0.5.0", + "Wabbajack.DTOs": "0.5.0", + "Wabbajack.Paths.IO": "0.5.0", + "Wabbajack.VFS.Interfaces": "0.5.0", "YamlDotNet": "16.3.0" }, "runtime": { "Wabbajack.Networking.WabbajackClientApi.dll": {} } }, - "Wabbajack.Paths/0.4.8": { + "Wabbajack.Paths/0.5.0": { "runtime": { "Wabbajack.Paths.dll": {} } }, - "Wabbajack.Paths.IO/0.4.8": { + "Wabbajack.Paths.IO/0.5.0": { "dependencies": { - "Wabbajack.Paths": "0.4.8", + "Wabbajack.Paths": "0.5.0", "shortid": "4.0.0" }, "runtime": { "Wabbajack.Paths.IO.dll": {} } }, - "Wabbajack.RateLimiter/0.4.8": { + "Wabbajack.RateLimiter/0.5.0": { "runtime": { "Wabbajack.RateLimiter.dll": {} } }, - "Wabbajack.Server.Lib/0.4.8": { + "Wabbajack.Server.Lib/0.5.0": { "dependencies": { "FluentFTP": "52.0.0", "Microsoft.Extensions.DependencyInjection": "9.0.1", @@ -2264,58 +2265,58 @@ "Nettle": "3.0.0", "Newtonsoft.Json": "13.0.3", "SixLabors.ImageSharp": "3.1.6", - "Wabbajack.Common": "0.4.8", - "Wabbajack.Networking.Http.Interfaces": "0.4.8", - "Wabbajack.Services.OSIntegrated": "0.4.8" + "Wabbajack.Common": "0.5.0", + "Wabbajack.Networking.Http.Interfaces": "0.5.0", + "Wabbajack.Services.OSIntegrated": "0.5.0" }, "runtime": { "Wabbajack.Server.Lib.dll": {} } }, - "Wabbajack.Services.OSIntegrated/0.4.8": { + "Wabbajack.Services.OSIntegrated/0.5.0": { "dependencies": { "DeviceId": "6.8.0", "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Newtonsoft.Json": "13.0.3", "SixLabors.ImageSharp": "3.1.6", - "Wabbajack.Compiler": "0.4.8", - "Wabbajack.Downloaders.Dispatcher": "0.4.8", - "Wabbajack.Installer": "0.4.8", - "Wabbajack.Networking.BethesdaNet": "0.4.8", - "Wabbajack.Networking.Discord": "0.4.8", - "Wabbajack.VFS": "0.4.8" + "Wabbajack.Compiler": "0.5.0", + "Wabbajack.Downloaders.Dispatcher": "0.5.0", + "Wabbajack.Installer": "0.5.0", + "Wabbajack.Networking.BethesdaNet": "0.5.0", + "Wabbajack.Networking.Discord": "0.5.0", + "Wabbajack.VFS": "0.5.0" }, "runtime": { "Wabbajack.Services.OSIntegrated.dll": {} } }, - "Wabbajack.VFS/0.4.8": { + "Wabbajack.VFS/0.5.0": { "dependencies": { "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1", "SixLabors.ImageSharp": "3.1.6", "System.Data.SQLite.Core": "1.0.119", - "Wabbajack.Common": "0.4.8", - "Wabbajack.FileExtractor": "0.4.8", - "Wabbajack.Hashing.PHash": "0.4.8", - "Wabbajack.Hashing.xxHash64": "0.4.8", - "Wabbajack.Paths": "0.4.8", - "Wabbajack.Paths.IO": "0.4.8", - "Wabbajack.VFS.Interfaces": "0.4.8" + "Wabbajack.Common": "0.5.0", + "Wabbajack.FileExtractor": "0.5.0", + "Wabbajack.Hashing.PHash": "0.5.0", + "Wabbajack.Hashing.xxHash64": "0.5.0", + "Wabbajack.Paths": "0.5.0", + "Wabbajack.Paths.IO": "0.5.0", + "Wabbajack.VFS.Interfaces": "0.5.0" }, "runtime": { "Wabbajack.VFS.dll": {} } }, - "Wabbajack.VFS.Interfaces/0.4.8": { + "Wabbajack.VFS.Interfaces/0.5.0": { "dependencies": { "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", - "Wabbajack.DTOs": "0.4.8", - "Wabbajack.Hashing.xxHash64": "0.4.8", - "Wabbajack.Paths": "0.4.8" + "Wabbajack.DTOs": "0.5.0", + "Wabbajack.Hashing.xxHash64": "0.5.0", + "Wabbajack.Paths": "0.5.0" }, "runtime": { "Wabbajack.VFS.Interfaces.dll": {} @@ -2332,7 +2333,7 @@ } }, "libraries": { - "jackify-engine/0.4.8": { + "jackify-engine/0.5.0": { "type": "project", "serviceable": false, "sha512": "" @@ -3021,202 +3022,202 @@ "path": "yamldotnet/16.3.0", "hashPath": "yamldotnet.16.3.0.nupkg.sha512" }, - "Wabbajack.CLI.Builder/0.4.8": { + "Wabbajack.CLI.Builder/0.5.0": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Common/0.4.8": { + "Wabbajack.Common/0.5.0": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Compiler/0.4.8": { + "Wabbajack.Compiler/0.5.0": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Compression.BSA/0.4.8": { + "Wabbajack.Compression.BSA/0.5.0": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Compression.Zip/0.4.8": { + "Wabbajack.Compression.Zip/0.5.0": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Configuration/0.4.8": { + "Wabbajack.Configuration/0.5.0": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Downloaders.Bethesda/0.4.8": { + "Wabbajack.Downloaders.Bethesda/0.5.0": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Downloaders.Dispatcher/0.4.8": { + "Wabbajack.Downloaders.Dispatcher/0.5.0": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Downloaders.GameFile/0.4.8": { + "Wabbajack.Downloaders.GameFile/0.5.0": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Downloaders.GoogleDrive/0.4.8": { + "Wabbajack.Downloaders.GoogleDrive/0.5.0": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Downloaders.Http/0.4.8": { + "Wabbajack.Downloaders.Http/0.5.0": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Downloaders.Interfaces/0.4.8": { + "Wabbajack.Downloaders.Interfaces/0.5.0": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Downloaders.IPS4OAuth2Downloader/0.4.8": { + "Wabbajack.Downloaders.IPS4OAuth2Downloader/0.5.0": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Downloaders.Manual/0.4.8": { + "Wabbajack.Downloaders.Manual/0.5.0": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Downloaders.MediaFire/0.4.8": { + "Wabbajack.Downloaders.MediaFire/0.5.0": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Downloaders.Mega/0.4.8": { + "Wabbajack.Downloaders.Mega/0.5.0": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Downloaders.ModDB/0.4.8": { + "Wabbajack.Downloaders.ModDB/0.5.0": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Downloaders.Nexus/0.4.8": { + "Wabbajack.Downloaders.Nexus/0.5.0": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Downloaders.VerificationCache/0.4.8": { + "Wabbajack.Downloaders.VerificationCache/0.5.0": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Downloaders.WabbajackCDN/0.4.8": { + "Wabbajack.Downloaders.WabbajackCDN/0.5.0": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.DTOs/0.4.8": { + "Wabbajack.DTOs/0.5.0": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.FileExtractor/0.4.8": { + "Wabbajack.FileExtractor/0.5.0": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Hashing.PHash/0.4.8": { + "Wabbajack.Hashing.PHash/0.5.0": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Hashing.xxHash64/0.4.8": { + "Wabbajack.Hashing.xxHash64/0.5.0": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Installer/0.4.8": { + "Wabbajack.Installer/0.5.0": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.IO.Async/0.4.8": { + "Wabbajack.IO.Async/0.5.0": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Networking.BethesdaNet/0.4.8": { + "Wabbajack.Networking.BethesdaNet/0.5.0": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Networking.Discord/0.4.8": { + "Wabbajack.Networking.Discord/0.5.0": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Networking.GitHub/0.4.8": { + "Wabbajack.Networking.GitHub/0.5.0": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Networking.Http/0.4.8": { + "Wabbajack.Networking.Http/0.5.0": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Networking.Http.Interfaces/0.4.8": { + "Wabbajack.Networking.Http.Interfaces/0.5.0": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Networking.NexusApi/0.4.8": { + "Wabbajack.Networking.NexusApi/0.5.0": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Networking.WabbajackClientApi/0.4.8": { + "Wabbajack.Networking.WabbajackClientApi/0.5.0": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Paths/0.4.8": { + "Wabbajack.Paths/0.5.0": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Paths.IO/0.4.8": { + "Wabbajack.Paths.IO/0.5.0": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.RateLimiter/0.4.8": { + "Wabbajack.RateLimiter/0.5.0": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Server.Lib/0.4.8": { + "Wabbajack.Server.Lib/0.5.0": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Services.OSIntegrated/0.4.8": { + "Wabbajack.Services.OSIntegrated/0.5.0": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.VFS/0.4.8": { + "Wabbajack.VFS/0.5.0": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.VFS.Interfaces/0.4.8": { + "Wabbajack.VFS.Interfaces/0.5.0": { "type": "project", "serviceable": false, "sha512": "" diff --git a/jackify/engine/jackify-engine.dll b/jackify/engine/jackify-engine.dll index 79bcbc811f2994665e4ac61407b80ece10ff5426..52d4f8a02f2a26975bbd6ba8b593d256a1078397 100644 GIT binary patch literal 232448 zcmc${37A|()i-|UcK7Y+o|z;)Jv~bnGK9=07Vo<#q~wR=KK9l)xEbmnJoUl_y2tJJX5FcsZ&*_ zPMtb+YPq-fIQU8@sIA-CtaP{}WEE zbe~cVjxQg1a`(|kE?E+s+I`fq-R0Akbf37SdxyRD>pnR+=GbX1E%~XY=)HGzoMSF> zo%zLcAB$@H(3#>ixicK+0&wOu_sc_tC@O~{q*rl3vqtyMcOF6eSuO`zd z==I#N({b{BfL#WTH*kW16Ahe1_=@YV2k530v$<)-Ty91&lRGA@n8_WJQOx9y@swmj zy~PT_cstc ztMx*L0W>o|u(RJIc(UPz41-?cr~4ZUo@{v`!@wuL!{12oWYY^727L(n+WR{Cgy$>4 z8~?h0`}|48?u`yO;424PtOorcUSQA(r@?@QP8baaEp)SCgTZufTYpono$+8M3RvFLp*y^( zwV`Kej&uwnI>rrMrz_%ZZgqQ>=1IpOqGR09b-5z$mey3y(k9X|i0BwMbmLtSe=*&f z?pX>&ov@5SM98=yoZw1-6f>=vo~11$WDpTDt_l5((H`pWCI)V5;AVv7SYMe8ePt^2 zRT9y!)XRl{7>Ur!F(?&!Ic;|jnxr+7**qy*YvgpJku1jx8ETE}FdE5%y^x`3q}C)| z&wMHp{<_aU^o`f*bcvOWqDxZxs=6eFi!M<|v(BX`6Lu7O@Hq6KE7k*+x-NRaYSu*$ zSnt~OAnuJ>AZD+?C<)xcz}bXX$Gt)_^a`oaDIr;|LEq!y0htaS5 zhMB#Pq3)ZVP2aFIPioeEGu`wJi}yl?+FEoNYr*QgkfB%$yU&hdhvBb#qcgMn)b*KE z+mAf;9BNY;4>bz$m6U$!1bvNtlTB>6g?6aSJfloQ1r-@t>+P8aMgK4_#15=IU72c%p>G%U38AD zu8Yo*_jS=ZHfC))*MoV4ty`yggza7Td1NlAsm1dQ+|I!54cx)N9Sz)x@UO8&V|HjW zb3>b%ncB?E)MjR;HZ!w3CaWYf^qKR}_W?0O!`2myUW+xDeK+vN;<0cqWGH&AU8nU~ zmaywds8#z6WAw;@7c$hUeYi1t=Y?_6dBaJ< zeXuL)U7+XE!3M{H_XGPo({)an6x&~Nye?ybO<0$)z&5SRSYRX9Wh}7O>ogW9hqaD{ zo{73&i(aeOv$P0pT*e@Bz8JTiuY)VQp;RCKO}?@_0@ug%;=xCCxh8S)kY}so?}3>5 zBV3hM$5nq4SW+JdtZK9m;rF*X^5pLYa`s8u=?A(AWmNVCDqr3+<(yL42hpDL0!00N z5e@c}XDoiq^ZtD92o4~aoDU9^4lz;k7xB>g*1B5AXiEpMK2 zP6-aiLwPI2h7LhYf^6kbL|bwl!C`=2un1v$E;t-uCCWyjET#)-o3^8n7UI;v7_Tv@ zrGKi#hmJrwLq{Soxih+kLMAv0NY_6aVXxcnA0yE7k0pfpl4Tm@*Wk3F58zlATpymE z7aRu)sIq@NAo?ifyqp06{)Rq-&KUX&f}X*S-k}c>sd=*-9ItW$NPC8v;=Aagl%GP_ zJ9HZ9|5{JKWkSysKP~jA0rbBl{om^8w@T=l;(J1$75dMT{_pklTPO5P@nJB*4~8tt z{|@Q@QBS{3LeCT*$_RR>6X>5H{XgsJ=Opw@@gX?Sw+KD#TYBh!>gl&l=$Yb=5qdbQ zC?Dn~J@ipM{oI6}DgHR2hrtK^7Nq}IJ^j3do+*B-&|?IG{&~`WTu;AULeCT*oe%y? zLjP;h|GS=k`-Gk;e!I|j2t9NwJ@iRE{SFB|Q~XY$?-Ke8N&laE`W+K`rugH9euB`S zNBUYl{Z0uzQ~ZfSKS}7%A^oTI^gAc?OrhGNy_1C=qdq<4G}P(yE(twT{BEINU+51Z zeM3F{t_eL;{3$}efzTgDdbgf_w}hT4{#2pw5qeBk>7i6T{rrTUDSofeZz%L{kshmj zdpz!*&@;vNg?=NUhXGCxW$NkoNa&g3_X&N!&~HO}ubzI-gq|t>fY1*LJtmp-P_~|a zuY{f{KH3QRWB!GH?oay0diuQ+dZzdoBcPut^pI_OC|6IvPeRWWt~EfviO}yu`g}e8 zz6m{3X!1b6nb5;kNe?yE)9;tiGsT}J^qUL)nWS&7r{6!JX9{OJls{YO=aIgpp8kM@ zo+G>mGH^3U|d<&5|)MO zK+1xd2TUuNZ@`kmv;tVw$^Le_EI#?Cu&nFcU@2g^f~mZG250s%AJ3|LglJ^`gwVMA zTyTR5i1Kc5D!^&z_=cwJKsFnkMy#jj+DkzTV_qmL6jeDrp)Y0&S(9gQV??0`4b6>% zGZBR{G_>RfXCR8+ZlHrOIEV*ydP6Z&$PD)PZ<-CxKv_kvf3PutS~9jlE;viay5K^b zt)pY$7=${S@`_#oY>C`3Y@`iCRV1$31Vyap4*?9gZGGiy!M*37gGb4qbC4o`<(nv? z2M}~!_B#0MX@3btH~7OKE$`wXZBGVAAgG)R95xy)f-9MJ4TIYgf=-Lz6{fw$;N6&@ zf@vZ@Oxw_RV7$tL0j46ux~5}4vsLTG9w1i>rT<-Zu+fJFS|2pp-(KqjpR}GXF85_o z>D~x@Sh;>(C&#CeACU4FGXOhH24JW;l3<0?T;{Fs+Udm{CUxEU7~URyDc5qizSE;_;Sq4l}%#uVkBUL(sWu zUp>vV=NbG8!6>J`e7Gi zjAWbx0O2UXPlgl0BMxXb`xuiG`xtW$`cQU|({8yv=bSQmT;&2#uK%h2JEzsDl_p_r zTn&(sNIrLC^B$WRbWaD2}~;`u%xgfLp!F)xZ!VV~G&*d}Q->WJGw-Gze~_w(n3M^{}Y zFr}6WOsmTUX4I7eOX^aARZZ^0nPyw~WVI#M-5;|pH3V^gC*65&9%&acxCz1PbXUws zLsLYc8@mnLk<5pfE!kMiDs1dr-AK-du=Gx+)yBJpcUL_iFs1Gnm{#`)%&2<>LObr$ zk&Jp!M?CeAj%3x>B~nsf6nuS1aH=Wei>OOCOCDE!QD92lCNQmT z5tvc83Pk;1l1NG2B(SRU`Mc>#_~c)L@?bO54vdjA9(B0EXMqYXMNpm&d5PW6mv=_g z4L(Qg+lYGpGG;`c)#ZJ6o$}_KcTfiZ%AX)Zd7znlj%F@N`Z&?oOF$6!aYFDK(B(H6 zyqgeoT7$m11!?0LtcSpq&}9+q#d9GVb}w+L=#ngn+w1QT9*Gqx-FF(8m&J;dxm z1@Fb4t}x+H&)|n(6h;2l1WUi3knVETlLAxf`vTMI2LdzdDS;*RJ%LqC&K(zyAwI>r zW6jn3lD(4<#B((Ux0$PDDt?FrxCjtQ$DXT`4tO;o=(HOD0W*Kd;FFj@4>C-a_5^`h zDz+W8e-o{CKO^mN)$;;V>REwl^_;+xdRkyrlli;r_VUSUZ)`Jm8w+NRLlE1H#5a-o zt~d?^lM%%Fl=S8HOxu~kJ~4rA4rV61IcP#M-@)9+cJNi<)m5(vOsSs8OI`Z5KXR z+NDq8Do%jHbTWcCIt4+)=Px zPk3xbZ^Rq$kBJ&4_GI_AdL9oX zQ&-1ocn}^TPE6=j!-WKxrVl0&c#h@*tY>20HxwRSg}qkDGo|_jVsRl5iwc1y)hn>7 z$$j(}+rlTSEwSzlfy?r(2u9JJWJrxKMQy3`pOd-v%$Pvy&LM0^vKBa$kCDzywc50q z@an2r0#j;pfhD!6z^YF6(Y$7%hjGSF{`DfuYq$%6Jct%#z``zq`%_F&9d1xMS z8mR{j;}F18?%CJLjm6^xoR^#ejE}l2uIqStZ>(LaI)I#h9<_Tl4+hbzT!Xj|BhWvz zKa%8RWiLc(UISCFLu$`3Q+%jfzYqJ+-!qu$ZC~7<@xOq?rV$ShO(RaWPxIn9C!FA* zQtS;u4w!wMjkMflov;Wc(Ka6;5c>!M)9NUJ8FjS4k~&OaRg?R$MwBw2^zs09Dd&8V zO~+q(DK+a{1mhz=JsEf3VA^90UPKW4Rf+N@i?u%_1f%QbgwD8xu9uQ3HUYZ4IfMBz zfmYtnQ2*w*sW$&ch|2YELa6l-E~xy)ihVLwHm$H}NW~5apnd2{z^qy-5K1V}Q&|21 z->6On6njQn0DXvU!7`Dbt1cIqQdbE~t1AR%)MWxu!<7;#sm}?l>U=)TCI!wXt=1D} zV85#YSn5@)=v6AX8O<3w8#JOdXCYGa#!+i-K|;?kQ+!z5ex(re(waW3r!OS*OwsgJ z#M+y@Q1XRClB2!XgUS|3j*idwv?$_pK1N!9a2qhq{qq}xFCoHnW`8*&xSQvH8Ihse z5%^z0Fu0)`Qq<)W(N!Jh_}q{$+Z0N>+`j{49ZikFog}|+B;PpTH3oO#;Y}|jV_tZU z#Za5Y5b<>kO*agArqajkNdb;grvX#kZ5at86{cM7a3YjSW*|v}Y_B`XUf1k9?Rrd=_sRsm> z)V%_$I@v!$x0z46&5O&=k%>R+GqIX&xY&bRTWP1*m&E>K@zRe-%TdJbxmZ za5rKy+6JbNZLp>^{~jdZ3c|erJ%i(Vz2H8HdA;QWkgsWED>ubm9It6)X0sczuJW-+ z=rrRrwTvo-E&Imceq_``)nv8ipMCamW@664TM-(yz@5W-b)rkyPA-mjv9K{qH3yuDF5E+=&+Gq>@yTA!=#azK~*H$p> zZ9E+~@OKHivfydw>J1c!c*`9g4sVu4fW_3fo=xPk4f_r1TrDL#0Vy& zM*n5Rpd;+3!9_+kk6E(GMQU#9Sq3{`<|RWtcD+QBQAMwyI1UY++5agX`;T^W{^!7a z5fhM%rt^_8x`4e>#)7xJr?W11kID2hGDVtJmq{w|WTM>im$gqn9m#tGo~P%^cHaEy z@0%xcm)Hnr0CGG8|4|<xKm;ZynGC1eZv?K;+7>_V|=MoD^bH2L`5QZSOci%^U zalU&-U`qW!U|KyTFr$7b5WddSI+9T@>4>L(tRq?VqK-7GpXf+VJ*Oji^}LQWsUPV` zvwA@yCH17hs;-|moH&W_NgGt2+58&{ya{-9gP)<{{%Z*G6)2C_;|9>2Jbfa@UuXPQ z9sdR6Xe9A(Fuq*Jf64gmI*wk=2k=x#gK?pE$0>OtSQT}y)=tfl)kJVNle~ahLi+_%f<@?i>z{}pLq2^s%2<1kB% zzsoqR5aYjL{0YV@kOwi+g7=8V*ohIpB?99oM*NNljGh?rJ`vFL81Z`|(0q^P2<3ku zXt4al^B0n%An;ZHTG+Zp_ z!-&+pO=vkkMqU{`aU_u7l7wJAi{N<@NbqVxFga&YbWQ<<1iK?<>X@9jC~hT% z1kYL&P1dp1lnp>3!N!Q`GPJ{c9<6-h@Sd-2|3w5DbpaD;|93`zVqTB!|7*}X+W(&m zOsUrerqwS5mekJ#Ry7%~mC{V}$@29N6D@(BTuSD+ryB1=CTemU({5+*wV3fbUtf-# z-Xpoy6*&V?s`;p1ICN21Bn3fxdOaIUf394XlI}54c8rO#OI-g%D2QTb7%aQNMA-o*O3|Arg>Ir0w_EuHhF!5e zu1wa7EpcUDRb*N3Ur}8!gX*S91c)z32#El3dxVe(5MPN95&`0l2qBS=4I?bjL|K%H zvJewxb`xdBgr__4Bq}TsRB%^>kO&Z8jSvz6;_e6`5g_h~5E236-UuNPAnuD05&`1= z2qBT>hBa1cqAc4)SxCb015r+ips24!2#K6-GBMReiBIxB7?Dc^`5%f95&`1t5kewB zJRBh;0>n2WghYUNBtl3ey1I{qZ$=3c0pigJArT-Rix3h4;#(0yBFo+KL$E%I`Cb1% zR5j*wD>X(*Vw#p%&TkW$R*wVpVH9vYYz{a}+s-A-_ZP8|uKKI7nICGKsnPyoZ8Q0# z?^;S3JEIJg6VEB|+htDqL|{t&M_^igDzKzJ7Fg9}yf(@?g-@0pK1@a_=9F*I4!@2d zo>MlCU5~^L&*+KlFy@puV}i?pE?>=Hc}$?^ldS+YoTy+vz?5nim{y$vGpb8qNwo>AYI0u7 zB|m%`t*^#y2;TY-jM7))1^$Hu4ZhV^&&GWvTX{2)R^BEi(5-vgYR9%{$5y%>pHg3& zgg=}G3rwjNfhCm}Sk=jXp!w#L-_U?_gv3vJM#~C(&`)}fRZ$OE&(@Z_CP{g&nk+D- z))QD#69ravGRFkv!zW8VC`bOvzhmnfHyYKakZs=UkygGBf$dW~AMtddDaWng?Ss5} zgDgka8DHT;TD?Y)Q7;o<(f%`m8TFz-EZGHS)lUUBs+R=j)K3KF)eD3bysMB`uL`P3 z{aj$PdR>z6T0&m^0@28RP)^%ea>C~?Wb8jF5OL!6{b%Vf+`hvic zx>jISll!OYviaoWZ32t$c^2PiNgh{)0#m9g5F6V9Gipd+Nu4RMs>%J+blH5e_@>?7 z&MNyY$>XYb1mZrBz_j|cz>*pfSk+|y>AFlltyJdrR+;-r9#EUA43 zRyCP_hAxv&R+;R(9dwyvDBpu6kE;$7m{NxdOshi#X4E2qCACmsRg?Q?>azLdkE@gK zj=Jmu%RXB2xav58DRr#Cv^qv$MjbD(q>d6;)#N;F=D6pRU#u&8C#&qwNFG<6Brv5; z7KmeVff=<#U`Z_&Sk>hI*}7~##bxK5^P{!-&uJ~t7y02IVOW+6eXtlm0IK?5ckXOsS6qrqw?LX4L-( zEUCW|J!(UEoo9X>EhD0#hm{Fs&K|W>m94aF>@zNo539bv~ZK zP@(zcPe6I#-@2>z2xL=!7r`j=yqsrFn+AeC5ojfYPqvcNc0UQ8j|ucV{}u(6_+-C| zS4(aagjH8f6qr(z1eVlzfmNN%i_UBUpJF*#chowkgM$GCqYM;rMTSVQ41qmR zeodYdN9#9uL{GmgWP^z*X8gaJQH?1-anTFqE z%cE5d_bLxWxqG18bTAo&r)`9~aj-GN^Sklb6RwZ2DV&0^IomUcv&&zho}Na1$3w1o z_!}UxPw?eWkf@U;YV?v+dD9tSY$|95np5RP(F49rK>4Q~td-<1UFiXhe;zpRP5b9- z0O{n@{ssC0-V~EA(%@nNm5o5r@X*LAc$t2>OhB6tspU~1UWX&NTqjiSRk{Ilz@DOO9{`{CX3qkmvI*E}_7UcXehak4C@S?Tnz*xjGliGA`gyd_ z-y-`g{#N9r=7Bq}vNhuVHVB4(Utb7>;#KBIAxsIjMKqYpU>*XVPW0!3?fHBhv{+6K zcfix&9JjqEFP|fD3u*OdWOemd3E)3pgG?j)yCJbf;0bG`PTWLuOC}SpxEzo2nnt>c z8Fz9`7bFD~GX74eV5F@co%MGHR?hFJj9L62jjmaY|1QKiC}wF6QvNQaoK}J4yx|W~ zcii8*rA}QBr=v9wj*C~>m1ID{z5q2$)NTefQ&5<)y~=!p>JiizNVU5`4G0Q;wO850 zpaum6)8ke4G^ln#9nOq<8PqsI?PjA2g2FKODtjBM8G>qMSy7EpXRwN4n4l&J3f`z! z*~es@DyZv-+Si~q5ERtPtL$e`Qv`)U1m(6jYfgynQPo z94n}ui8{!jU>w0Jj8lT@5Y$mNRjZ)(CJL_{N?C1!T1XULI206m7-gME6kbpiREwan zZ1pNQ!xa=13{=Mw6?IxkP=^wAsG;f>6fBijIn1Dng4&O$MFurSP&*J6_2YCwjbVdC z64S_@Hw?cwQ83xNK?CTQ6jH(AC=K&fnU0u$1dFE6^53V^VOvxFB)vB^xj#6PHEG)o z`N}&>sl3v$b+kL6-w?{#QkewAB4Y@GZ*>xVLrutDS1`6$;+O$6RK3u zbKGuYIOw+b)t5FDmj*rWhg>xe`?9Qa3=h4l@Gu*R19s|HNcZxH5aYVYI4!Q)aQ!&^kDYSrKW!dK<0*h2+=>EtM4DkDavyQ^DHxY1|?6w z&U0CPo~U8yJr=!E?*YrqD`8Jh1*bu3Zgm>cYVDh14v@<{{^`K>;XQ_obGYj`n9gAf z;d#Rr4s7LhYz0v}mCI3oGhA;tv}wfxTsPbppsS<9hvMcH-1oHGrr^-0-FCG(fa49c zf{F?473gtZ!B-}>>V}L3P+oe_SFmZ&**xDc=*Oogno$nC%k+vHAsV+*W!BB`oC`3G zK_?jxFhk`7Ohwvh#>|r+oF`5*Ij7T%K?CO)UOcWwxxA6zzXG!fF7GjceluaMge`~$ z@tc_selsSdJh)~Z!o*0Y*FL?dIUc}`K;ir@X!M(Ihct7f89sI)BP}06@oewF@frrG zF^{X8Ss&9CzLL@E`Iwo=X$L%Y&8erhL=MBLCvOe(ulNO99x?%!`KpAq(yn!!dOoHz z2!jIWXE_Es3K?0v_6(*9nX~k9VU8=RT}lU*A2HptbK8NfZ>(zm9z9_ z^fR#)46eM|7BML2Zf?k!Tb+Z@huY`;D-Z-%GPnu>jnLJAgIkNbu>v=z=1QKe5w@mW z|7K|0$mwoKY>QhZsFh$4^a_nxP{=hXq&ZSA)=Bju)K@Ce3w&tgA}&qYN=@<&nL^sX z7Mb-TEwu-NsNW%&=~Qly#F}-Uc*f~6@*%w0q;9wal7?;oC;k@@3`{g+b7Qg_fi5+#c!s5gJFX-37eI}^+%X$=qf%Xq ze-layZbndSY;E*!LBv=Lw%IK-s=W#I$AcPO|5jRz&r7vTC^ouP!deN6rTdZjP|~d| zGK)_X;<1ob697iKi#gqrLazLB+?Fqb5^ec6mjhnMw3t0MYjpN#Cf2~--%*kq{#Wcx z((L68d-?L@jiVkh>>bKN+Y}agv;?~YI9}(pJ`5IiR-!Bq95v^nNmq-d@f-gd<#j&kp{pFNeOKXe&Wkk>sopDQB)V|1LI-b&;Z`YCc9o@;{#|5yxs+8tCm~#=+~BJq#Oy$ApAEZ+hESNZXG)Y< zxf-mo5?xqq)#SW+3sI3=q6>>~6Qnh4b@_bM8r-c*kFk`+*`3>R97TMHq`D?n17>9*gB%0hak_F z^!hbb%{#6R>)(zfHE|849A8Htl>*hW%9c_L8#KqVXAqG>?<~>?3z1t`DWIh%xEI_G zHs%Bl;9bJFAImM9jncce6l&gQSmb?d?^ojX%2q^F=Sg)0@Xc6!%{#dcZB=R)uLmpq z3=B?`K2$(f**G63WgN^hzJ|JchMD5u2^x42rZ5@UN#54_yoV%tnId`B9*7mvv&W}6 zLijLyETa{;nV#Ai&zJx?J>VGSK{k177}4M%#EPA*o&MJmfsG#@+q_P70Emr^?{s}Q z)cq@Z;YGM16N;T~m9SPirq0H9`VX_n4!6*u_CvbkO<1@i_}~GS^HcmcVAz5arX`b( zzYUqWiaEPlyR`E*&z%r6)1?jsy~gDwmIo&@f0uZ0#Wpu& z`|*wD+0cph;2Wq%^#KDuXqB|M)*zLMF(ac&Wt=dwu2R__H9Fp;X}h6z#@8l=Npa<) zO5yYQb(BKMNTFm)p4ZtN=3!N0_9w1_q~+xSom&>jI$HCvs92e**xnO7LRHEY z+9fj3ZViv}HY<(Enzw{yd=okRyHQiYP}jVm4j;{&love(LhUI;Dby#X5XL?P+XV-S zH~?m{m|4?UErQe<475@?rMvGFDFo zvDyXC(OAu_K`Ymb6=PJbJR2*3bswt~j^jA^Z51NCS4PwBzR#5JqwcJAa{%+9EK z`Uk8G*J-rkL;aLMVZ5!U>uZ_l(Mo}>RxaQqA9FIQV2%_{)^)g2c_5gkY4g{~YN7h3 zxcX5A@hfi~1%V&55kX9_17fz=OH&t7+pukdS%oQbB&x}4-f2;qV29f4 za~{srk1<>I3^RqV21f&*T*z+cP)B{9Bcc*}hM7`V+v!o7uC22^=aETmOi5}hPK*u6 z#8C{$$*}<`@RRe%TJug}S+NO;uuJQ(t2Yx-3U$2Rw7_Jr<&otucEqjj3S@KxqkIJz z()TaaUL+0~%590+T{5IY@S%1CQm=ADpUN6nj5nAGk+hAN8dwHGJ17rWh%Ehwxv)CFLv+ffwq zw?y5T;@?N5xu3xU2-H;IbXM7=4ju<(=k#cmvUpas#xbju3&k9=CVNx+Xk_Y`iwy~w zk2uZjYcs8L?zy9;EkT-ut9t9T|CLIdn6)Zd}ut%v7`hpy~_7M+B3`){~;76xjGgW*Dp*Ss1_y_ zx+is3o`5D4sTVI$GLc@0N1H74zK+Zwd3E(9ddqkYJSzW5R0?8Q7wGator3QJ-&26}L3t4*r{Hz~tQjkB$B)k&raHJsEhG7vO(Gm#NP@9C7@)};*ftwZ#inTRL#Si$ zGqhdV?LDhEK0&5&dhAM z0XKZxs8g`7dc)Ts7u6v#XyN`kCT-g-M#+G0mB$|?$OX@#isiC_tmEbDnFl^WexPJZ zd91FKT<|;+C-L5%we6M4q- z5+Z0b|32Jy^9@ZMw_0@=qGj-bOxgX5fO~n=jPKQFB$1u5$CN*7%cKn`>y>)4T<}vM zWXJneMB>#SuYsN5=Ln>O zUq{4fG7IF<92^O#E7nTs){N_albQ^}2~B1KPVcIOwNemiGV|eEQole+&}4WN!a?g+ zHV2)}D?UZ5@IyOqfXofyt)}&N-uB74RX%lGwkZVJgJ0kIC3&Ozyot!|#QG}`3q}x_ z!goR8k=v&;SYP(YhJ9#l&VLK^ZV2x-9lXu_?;sG(`xPR%Gsx2d-R7)-vwP z%_GOTA+b2Cs1nvnOY0h(^?%Ls_`Uxo{HMD58_@rgLXeC3H8U`VcZ_0O8gz&>4r?8aU-miDv^Yk4<)^i zl3KFibZX=ss5AP&P44m&m#yHi;V+qzS8X}m@C3wj{_l|upXkvpP^13`GL~!2=~H2v zjtj}5?m*39RxL(O!y!8GTkOCC+>lsyAfa_6EbL|FNLczS&;F8Y8V)n5sE)c)EM z^4hbyxq*_poXw{oA*CJQ~LT+@_`B7rf5&`a@%?-*@Pwh zBMIhM*FsGCpNL1ghUN}2??&i%+m3>M;O2J$RBCg%SMI7U1g6w%fh9FdU{xpk--gU^ zpPoY?J5JJ5p)9ynG{I%E!Y zs)bNQU_~EO3=wfAf@}znE?xd>oh{VmgiM#83G49x4jzY2!m}(?s4X?`RxVWjfrOr6 zrug4QbF=(yT2o8SyOR|EWb?jK9X4Sc%H8o;X zexEhUMtH~s4ot#YsajWK*8h}6!uO}V3!Z|#i)zGmhI{}=Q_s;ZB4{HILv@Xf>O9bz z8gVN91J-y8y3!4qz*A|$S_!VMK9!a>Hu@=)G;}hOtRDCx<%>@sqX(EmAAhADxQi6{ zf^y#fA<~AH)aSpAsh;F#ivKief_j=Sk)}~-nBqSJ8vj`Y0oHKZ>NO*RM)CwyH!`Qr z2dQaf&h?)I`HDxOvu?-)cpz26TIqtiM&|q$lr)4L8oQCp$-o$4fGPg-WO8Xe&6i07 zs|G>A}CRnpYQ~EF)KZ zkn|@YA353$i7nzH7@@U}7R|vMD3A*#k#2m+4U>qJ)TYgajI=_No=YZ-ni@|jj_n6- z54*Du{<~q1j2{@HZZoGuW^T{O%waZ6Pw-1E_6HoMlhviQRP7cE*m*)pk^) z{g=>8*ik-dJG#gXFGi8_$$?a*9pwfcQuqY?#_{jK*YRRMF9i+k=Vbt}pRgkitgeIa z%Q%NbgkXjP}xDa#)hwyhVPO=G?IA_-gO!tQ*YQ2*o!ZkoYU z>R<7J*h?nxrWv7Sy_gRr(R%4RYaUi=UZo3d^j|@CxW`aVVes_TF=$$TBr3}CM~!ih z@3WZAg5M#K_dtX3C{Fy&sH~UPQWjq4cPbN*tH-TUmf~2`WLwu7gNaPf>Cby%;hGQD zvk;Ss=q3Uumg@s@wveK(q!Tj*81spWt5CjfJbujTPmsh(8$c`Ycjeqel9>8zOjg+d znH3dIXVcxSKLCguX#F@xl8UC1?2?!)WVC(pOnirr+LdUmd1i8=yBb;PVHp>(42`IH z53+xH$-XXziNS_=_VEGtytL7OqT*`-a{;LBV=yP z)k-JB0^?JX_y|1SH$xyR%K=1S&^b?PO>0!wu>h@Un6+>;+rI*K1**>k&RT@E(&dps z7foyQ2T@WlZdB&oLSC5UjY<)Rv|}2Qc^PR-d?yI`wduo!ByIZiys0^8Qr9OOG`aq( zWCA6%caL^>yPrlWO4-JtpU&!NXrG77vS1a@O)k-Z$e>&JQqhMf8GDT=% z>UCUChgctc@Cb$aFlo4{)Y`oAYF(?KeUD`(C}E5@#H}Y7V`*EPwn1&WFGa?M7HyNK ztcl2&X5PKz-@jnjJ`cDIV+mVL@f}NihQK)n&n>V$h&osmLu`6Ieccj6Jy(apBc^%x00_y(T<)8xWkA!-1!M)VT$-}U01_5N^|u$ zYFXKEE-J<6oUn2X6XJ!#EzR>8TAc!uwez25#6KR}n_ zJ5flW?@nkfs)c90sK)24NP}Cf=8@E=mBmYilIxJHH9K(ccDGt~;Y~Jqz7%FP?t| zXpd{`XF7XAf$&Z2c>Dq-`*cqNhfkr}Ykn$T%AcbP=QukO3(6B^oWVopIC}t~$(#K) z%J4cCc5HtV3UDjmppyhA0eY^(dk|L~{}JL|A~^UX#C=3?VB=d_2=(tiV%JRu>h6$I zod@*|*dHjiP;LwM)WedKp-kH*-&=7$Og<3$wh;R0%R4$qY?6!1yWb+L-i@#oD)Ks2 zxGTEqz#_}D=fVGem$sy zc@Vk`#qyW%C_52@V+x)m%2R(shk_c=<1F8|dJ;61W(chy0D}aNNxqEwwtjpZU<=HF z>~TSb#N4R9gAiJ`)fcFIom-tEriXRTMsK6Ln9iR#@#tz$HDX&YN!kIu6pE4j#GLu2 zb}n0JEZ{C4ydHGQ*ICZ~C`a$La+QT~6&XXVHF@=2(rT@N*Z3y2W)^u4nZSh>p|#yA zT9cRU)&X1}qXaPA3r!shL>m3LU`laBI1x=qaoMfI`-2{CJFmHQ@sHq{*0p2{&4re= zw~l6W4Xs-s^8Vjz-N@F(FjZo z1o}a1&E+R<`}lu8{S3%G;}S`= zX{#sw5uJO|kQByh-)zIRQd(w8@0lQiAzV5|ly^}Ho4hPJho1Z`n>-rfVEq6X6{rohNAl~Nzn!Ju#(_tL38AtQ^EKpq?UE&G0 zS9Suc>PJ9fP(Z_IdS3*1f@jID)Yfl-3=wR;KeC8)U5m^fGyCs$S0VPh#TQZhR-1~`&v=h z9jTkm+aK?i z1}JGi6SyftXt{;b0R8%1e|I(%re>^`DOXwoqr?X32i-&%rE&PBUdZ2w;KwBBkZypl z+sHSP*faEX*)aOL(j(`i38OZOMtcp7;<2?GuDwxi$ZGk*Abj1%*w<~W^L42#v9C+c zn@46us?;Wmg$+bGg%XZ5N=rF9jFJt+RZ^2NU~C|ahjR(~11eqT&(a1CL(wkBe>dW0 zJ^BoIvBpN$S&Doo$=LWQk}x)21Hjqh{?PLj(%kT8K(uDe>0HL&gGxhPfw!!+elJ9f z*0T(1{p*DK$1|bz?@(!)q%bB>>j`V6Ya*>@K4|^kC<%_@&Q#=GpfjVMAO0M)W}Daz z`JfLzxxD!e`yMBUk#Sz-;By~Tpcigbd)STWSWz>cRlh)ahH+TaUx9HRD-ugnN@%Ux zgzIdyBiyAsR7#SjM?EPYlS$BV7uM|oZwB#fkN>WuuUa1P^jtKRC{<%0s;GmkoDA_qgHPAkl+~VKlXWAk@EniVaINuz$rpGN75jRRm$JgawD*Zec!@ zbR>(+(oOk23kVqw?w<{J=LmlREr>>{8&Xc@QU{i3`N~2ds9WTBAu4N{^WPIo(e6?wj3R~Fy zI2t)dWX@P6wb4HYiO^ZwZ#opZySRRC_$^wI@{iTD)bCxg4u3xp`u#^FLBIbDfU}<_ zG=47rp`^}^(1OSDe<#XXkaC=hM)}9({ST3oud;Z-aVTG$rQ;DX(*}#@wDB)O{ad}5 zHgE#czv3B>_J|3bHVA8_n<6b^K1>@Ypd>m=f5ju}`U}A5b#V)R4RsyzU)1$C)}{6M zB5LO?U{>!3=r1z-9qF1C9J}b7Em=Kxa)im;$w}1Ao%BsKW1jjqb7~>L;d-A!_&ydg zZpZ{WT!hx#Dfv(oCr|aUR7+{EOSL>>UPHC>*H*2mm3}BI<^c)Y?Vku%;O-D-WOTbd zE`=z7_T}YAUn2jMS=$!}pJC<-w#zVDLUw6zvW`>a!4k&1f&f7ML(1PnhBM>3_TUtx z1xpd|ce(sBB2Y8FT&J6wSD%npOBfpxzo&$cz;e+InZOMR!deMks*M@*p`;3n%(;b} zsaTBIkLx<$w06rKEGra}wcu1_)(y>ud&e9LIyen!db68{9%i$fFW_m0o7I0%f?>GX z_5VPI_3KMa;4TNDwb?BUH_K+X>;&Nx*urd!i|B?V2~MZ5 zaKgZc${Bb#Tgx=+7X37ICQ@Vx(bnxa;~f7iJgfgf9#hP0JO^hpc!-8>OzW8F9Y?;+ zr&~2f)tEtd!x-29fUWuktcV*jp*Y5^5?b##N~^}mJC37rplvwco;Nm}Z|~{K#;Q+~ z%Er1>J8WG_WlX?6MU~L9;ZoUHZNppKLW}x{1-K!Ba`0_?H)MlXsgo~;t)Wh2M+4WX z&XBd>=qiL9XJ3K-a;wCMKpIA^9WM&-MX)tgf7UuzUp@jlTxeN#49>T7t0n4J!HxDv zjIKWqy2Scr!(=!HiW+bZHDFxpIPLO*MZO`XSsbStC`hdW<6Qqw>=hiAKtfC?!uKS! z96#xmaps&v#$W4>cJ=TE578f_p$7AnB2jCs?ecFH3*2Ion%iWmfQ6z-0T5%|Cc_#} z%mN_#EW8y8+Mk^B|Fr<)dCtR{4LavMgE{iIh}@6)rW4Q6-xx$51@3A5Z;!ZHv;7JD z7~eFy3!;7heN4ucjt6l|DmotARNoniPY$gloE&1m9lu+qPY#n5o*Zib_Pfv)T!O{? zkLyyL|2+Wsx3?nQtzgCOsVp#Vs6$+gN-LJ55h0`TX~n+u_zsObi!L6v%XUBw*-+B{ z7^i73Z9inNP9%8H273^B3}lE`+^~BC1xh32-Q_=1ftp0enZScKLhD3=`A`xDAQzfa z@!(F0v0mv!LN}!5VUNbT)ggB=wlTR6vT|GPUhcDnhUON~BtxX5d1P~NP8vUa5tUZB@ zgF{lMEd8EC68gOv0DA%(VNr8BrR#=Gz{DT7YQX~r%ou`pH5LFHWqY|H@oTWublr@l z=7uKgq7zsktmYB&WcNdMDRz)y6PQ3JkkGQ!%!iT&VQkQ)IMql`&Y4|JD2--SuNN}q zMG&%H8E10-xhT3dW9?~Xu%?`dZqwVBbtySN*}cr@#dPBFwX1R90h?27dUOy=X>fCj zLg3~U;pz`!>6vN)WK}CDjR3F*@fQj}Z%#3R+Yp4-j++P|I*4VpYdMG|m+|HlIoF$0 z(tT};d`6p7!jVR4hN3MEQMgr7BZZ@#f3_-j7a}YFDuovR>l*>T#tq%T<jUohUrmnpm+{awWXn;MK` z{>TS!Fw7@c#z2)DCTYp;+HaqCWCY-gLt1 zh6r1+viv(u_f1etd@vFyt}F>Hf0p^MvgAglQG@MvT}2JZa&QIeuRCRxHcHZ!NW#3R zg=O82q&2E}SZ#ZitC6K=m?{20&;b9R z2zU|VV5mJ;_$;l1KbE?xC^(C)K@?ZQ-v1Y(7_JiqHNkr63k}&A7)-is4<=pS5RfKv z(j||abP4sO%WtV&05QwG!V3|>o&a}UG`r1e6Bed>4ohq-kNa1=BV&yTTw)Vildkk! zv-!#-C0lGNG~q<3DLxTusyh)Pm&GxKF>?EfoHvgg81+e;HdUhR=4`x8I&EYVeS$7b zu9ix(9UY`kqo=qo8KT)5`+`{H@oypiH^6@t|10-|}MH0r_ECBeKySv%&P-vUl0?%#;cQox^ zLnY@T;9AjkIDFS3VtxsL+37lB-vOP74HR)5|AkH*QTU=Mp6NFd)=JBxT^(jZG1sFQ z%ml2e{D`G-#uqf3Q}_@l2;{IqTyd#UYKnYtIps;vk#wW8hA-Y1pEZy_OAcA*W6W!3 zh#a;=66CNAfO$vhXvlAC$Q{2|0lbD)^ST;OdW-Maea1O%7_*N!B za06Nwd?6CqjTD)-SgeU6+kvpU6+(L3A5-s_$hIF7C^f=b>Gnu!%m=B#s?pnC#HsKl zOKQ3BX4ZHMg8Exhm0N*Q+ab4W(I|M;v`ISwY|)G+qG^H(iMdhjjL_8bkyQ&mf`o+N zizw3cPc}RPtl)k{={j&)@H&FqSl=!n!zkO0pk2)e;K9tLkg01f$R{P~8}XGdAs;p# zrGn*%FAlz}F%-183c>9fA#x7BV$t7$cyOnMxQmFdT8O)exQ79pk-XYfb2RPLYJ<^1V%^7CwYwB5eT)UgzuCu^Qr008xS$}k zR#ei*(G`)AK3>XXN8?iFdb`=Plhp}dghd@W*>u&upi1WFgsKN0;gww^(`g}FYfeU7 zc`$OnpH1h*^#|BAOhst4sE!Z?Ujt&K1k=wBh3S`%TC$H}`bBoN_6z9woKJht%||i( zoB=GuuZ7d1_CP)}0PrkL16 zERHRV?U=THq=}kGzOK~(lyrHUs5-QtdXMW61o}Y_2waDF=M%XOA;SaT&ILhyb9sAG z$7;^J{6^>Q+3+Y)DlIw^bp-!G16SIU)9;#Td5lwgvMg;7uq9WcJPw2XaYH6>OODWT zVMHE!OU?{2bPQQosbfOc&pDZJ^{YqVP8wftFjeT_~S?UJGqBO~i9Bb{(DD&`3;6^1BwCJMrioi|`fv z^LrciT)R8w1Spd85}=LeUg9+{v3%7pFr(hUzq3HUNU z*Sb42I?mZr&ta?wUr+#mPk?TA&cQ#X59eq+2&DP@ytBVkFv$X;8S#cYy51GKtRzhpDmHDF0 zRu@Bztkt{>e|N770+Cnb)G9-LX7*T&W%E20ym3EHOF_SphxJ%~?$^=J zng*YyKz{!*VD*U;*PjD;f;Y#RUiVO5oxoaj4`Kc3vWIFg0Qk@X61e^(v}Stgp}g#! z+OLMo%=ITtV!FyF0FqHgT5c=$P2)Y&U-K!%LV|uI%%Z! z)^9dEPNpj%_$pn11Z5bSZaASMeL#k-Ar8y#c;#pd>6 zOQ9J)i#9HzoUw&5gv^(`^f)pOM_)~amI2&i*A~Zc)x+uzVs#o(^E{5hUZ71uOtfBu zJdU(&>O_KewHN@e5%l)*-NKWiUeW^7YE9*giq5j=jZic{T8SB90zPZrhsRa)maFeI z=M<)BSvMh zU=(=a9*NB?6oUKFi~3ln+`T94!(0T$+T4#QK9d7BMpeq#_?Q9zIQY>oQMRkkAk&J; zthN>3x9-Lu(LbtKEXuDc7TXF%9LO6wY`R2)?g>;G8<|#%xk9nrw-=6X+r(o1w+xz! zE=NTxE=Eu+6-qW&?V78OLi^x)MogEYw!}0TR!8(5m6lRilU9`G&s*^xq%p|_9 zoT8chx!77J@axKimX9lDvQxI{q~GL~R(uSH)NA(s$#b!*&?VbG?Ca){xn^YH9B{MZ zc<$B+TWxy&ZqpNDn>I*oN`|6X^ul=kirb8J&thJGn}>}^xC_pIFr;Px^1XSuBJxN4 z*0AN+>v4GB$Hs4r=YHT1v+&IHBA#0S-vC6B@qW-0JnxO?G9dOd_+2Bb5!+>}z5vA&P1eF);nNALOAvBbI8AlC74DrIGJ&%)q4fe3^I?9+brGWc9vEllE1x?2X2s?W;g-jjz%iB^R2Eufm%A@6cv{m%;ZC@CHF!e?|@y zm z04qjxxFeLI=#FL&g-qZbAwug%M?`dE^pB3DW#M|-#n2Lc0RrzM!P3)?mp9l4&fODe z*kCb29T1`Lf?j(qRV(29LmCkxK{rC5=?ikszDb+4HdvQeO~xB68dHlsfv{ZL0#-%t zZ$b#XFJrB_5`!o+kNm7|^NP6`HPKvED3l+uDyw6NTQh~iY5PMAgIOt{KO;_bPdU9G zCJH^X*|Wt?K)b??wfB@TVd{SZqd6NcMrY!>!jn{#7a-X!Z{{e`g4-MA#LA^5>q$C2`ClA`ZKf0H^{exn)u>w=5L)}nRPlyEE67?$}$ zm>cSGeJYh4D=bfO)rC^Pcz$Sa#N|MQ%i@}cdv#vr`>bo2DSj60=mKh9j*iZ32v(2aPDj(NX`sIgy%b(pJt;r1STVKYfECc(p zV1X(6Yt7G~%wn4n^A*S!iCHrz3rTZYM4c`79pIeO{T6zrtMUYjDpJ~e0%xRc<zD()Vz!WNUp(gn z$fS@hcw7HEQQMtSWxR0lqJ<0AcCv(#i#_GXKr?y1CCwLsh@~mD+{iSt#YV0Y<`r<(%*io$ z1-Rg+OenRM{8tfyFGkm2TlkVX6I8~+mt4OE{j}m-%)@TTgks6964pxhMF$XK;YOVuepnQpi zI!5ndv>cpJ?IU_!zQ#h$k>5<8JZKrnNrxo5l=H_xR#32&$CRnK(8YUW;$MoW+q6F; zZU|h-ouG*NeSuX{>-HoeKZmzQPX*i%3iM?_-reC}goidc!+4JTx5FVfto)o801vkC z^wZviFY|l+cN;wXG&)A{o+Omi8$Jt|cux}BfQN%wZU<^;cKHs*xBNhduPZL62EGP` z;5P<=R->S!y|#F7a2Q1KcLF_!-z$bAhkdC$%zF~>J;e|+b zLmi%vxMFeH5UpN$uZ=VzQzOS{320Wo*T&kp*9QL=!{R)I+-t*bQU408g)uCcz`Zs? zYp;#@?7cQtTl3m!2D=~`z1MaLkZ#D3-=z{}>-XALViucaxcZ1nFG3dCx8l{Z*~5^B z#^K9hOnD47==Htzk}};m-I}brlojjN-~?(MTZ4HCg)o69P=waLl(a^lKxuxH?^MaY zl-zK`-jp<9mA$D+C^{QrbzmAh`UkZ%Mf9C4X`%RDa^$wkjuM5IFcIx2C0K6Q)$J(R z0$TH7-5jxps~6Qsd0phxi}fVFpTfnV6xF76v`tB;i|lcGZ)>nQBHuw;9Z|GqoQO`w zAII$1pmN7d?T#PH1^ z7y#!7_+N;gnt+m@!haPbkY8205dX)+QQPA8^w#hxD`tM3nLm%1>GLyS2_s|fhD+!wYW?$^a~Vg-8s6373kvdeJecRQ(p-E^WqgAuWjK1UUZ7iw(_ex zUm&b5L)eOUb_&$*%f;7Z0^ivov{rb`hmvH4*Hpd}jnQ}Naf#BAse0| zT20=p#S=%JwRqB8Wz~(KGF)NQwUVokAY6PV3JDx_gx2CoxXQ}n2}`@Bs3~$3DAUMM zARpRMz*oK@KYT>)Nr1q(31lsD6R?|u`)-o@Vx5d6p(ygeDtxxmyy;`?+Y?R ztwX)E8LSrX9iIzJF+jnDG6$`03{fs?bhcy8?V$w6{J$Jad;?0iaMZE%dzj$=b}XH{ z8m+`wx|qdPX#k=xym0WuH`>@Hd{@v$QEJ!@rTsRB{!ZA5ExnIF{WSV*Aew)^$RpFh zT}XWs8TcTVv6?yGNejjLVpB^TTe*ytpp8cxy?-+md~S&OYCiru5s&^j*Bt@e68v`| zPJb429nE^|pH8HIdJ8bIe~MigoY%e!dU*WN@40V*toUK&7B9es<-C3ircqzZ?)NlO zKQ-wuee-(~_!S4Ip}BEzQ$)8!6knX#1ktS!l~+z?;9+Y%VB27D8XmUc101r!f&R@J z1MIlsUF-g7xd3O}j7^2U;Ha9>E-az3*TE>hMHOHH%xJ2un8h+!f25n=O{ldWgCb>< zPhT6?%mr`IzW)+IZiVP895Ihrsd?aabJ9~K|tHk5+GZU?~oMpvK?x3V7n?ymsjhB{o1xMDH*=pRCB zZxo&(?iHrXKZUL)J9p5~$7NJPQYMdO7kIbO%%R-7B?t7w?9LN(u+uURRORQ_N1O4MPJRI2O{?@E($f))AjR)u z&Y6hyZf;jAM3UJdQWuIKB&*SevIzOjUl3HuX)JWFkAI!gqlPe}f=+k4f-W zp{MsjPtO%Sy%z;QPwxl7z15>|xA1SMFB@XmrOO9V7PnxRK;8?_h8Tcp{BYU>AP#=Z zB7cWK?zFy-2s|I^9ZRI%JxEwZO`><$#^_%$TjoI~Q11w9rAMM|5$1#5{hmdlfb`H; zfesjhaSYnE> zgpop~az55@{X_TEXXs^yKS~>z;;#pq;7@F^x5f7$cUZPRGd28>xa|L-?!Duqtg=7g zJ2T15WKt(R5C|>dA%$K8A%q%w550qnpo}{a+hk&}fCUREu8WHW6@|68wSkSbA?&)A zwGn$?Tv1oCY<$1pbDwACnIuGhzu)`*@y_Qnx0iFzJ@?#mZhNHjGQ9cb0t)zBa1*=a zDbRO%0@m|naXCR&)ik|t6UvVEca)NYH`}6)qIlyQ8j+#W-d4#o(Mmiau-Z7kH}ox1 z2fkyL0sxI?CiX`L+25GF5266R%zgv#?vc4$4Os%AGV96k5cN*F0`I8-1)4%)Ze_KI zjSLTE&3~7o!+4Nx5eUlK?}Jx#y%h-iJ^r2GH^&W$Ffwk6Id1MlNMzg$#0tzIv_{^t zE$&%c#uN|OQ()x=7jk^Xc-!_%B$UmVHO0KGDXTh|I(@}stxK$)B7J(Rl@K33lwmIZ zwDO4VW2mzEBf5_>Oh2NF_He$M4x2x#I`c{0y^^^vdZ7$=X5X-&~J_kPr&O8JqeTh1a7bpxyv*RD)4#2L!LquvONdOTyMG=Y1X2T z%Fz$b+Wx?2_<1Qso6o=z;<0j(1QxfshZMg(W_`nuQ>~gF@T3CdGUx2|F0PdxTeHUPgck zf&JDk9z#P!%z5!lFakC%GpzCV0^E4F4d#*|cL6B*YeJA=<0kBDzLd>==A^Ks0o z2HzET*FPkM$ruBo8dJ_d%79jB_(5wBAwDe%12^Tf`M5n8DpmOCtjk^nRkjsJOfnO~TcP4EDW})F^vDxGJdrgcQ+nT((_kkIb3RH(Ow+5U7K zMD^$GEX+mL*NEfSVTk;)voOz}p_h_N`kPtTB5Phu)|R<)ty~~$5#l=FMctN14k5&K z*L!lpyfgK@o+%TfX5rS>oUG9wFHy7gjPK&Gr-mWL^H0s?xsMR`4gTGMFjJo61a8W6 zltn!19mJh4vdC+Qf-HjZNqy!FPYA(r!5c_GJEDrrMg-QnF?g{Pd;#3f&>P5rXA)s% z20VxeU^~qhNZ|P9ZLtf$PWkHJg=DMx)j!sRC%dq+fVt~+n0)oG7#xGI{xK9cZ)n+W z-Y^Ra*@!}9?I)gVKT?hq0M;K}`-ujdxU%ly-FR2z9v*A8;Q?pvLCa_l_5j~T6!35u z7VxkhdL1YPLdPbe;~AjCnp5yPBGR;cW9}kB3*!Plq9V~k1unT+svaxh6})$8*S!Oi zSMW-JgTs4;ycvNRx{h{J*0a|9&3r&=GD^ZSJ!`^Zf>f2ls?G&|M5}Bdw)I!@bXD=; z_mI`B3ac4lIP$zDL#a~Iww-#Edh|yL1mAM;QA!eEV=g{MgDu*$>Eb+9i)9e|ni^Xh z(J3(Zx*Og{NJPBv>k6hW#T0~(hlz@iX4-z!#tyWEbIqw3a=k-k0b^6qcdkG>(%|xm z?fevFZp!|Tz`jt@*w>~0f3Zeao57Y#JK|Jjq^;Z#k8P_faBnMN_hA;+w`t@AN^K@f zD^GRUkpwLb9}W_K8-Sp4jti2N_Q0`nfHlY!2zHNJ-xX+au`6rZt4nF zMkBmtn9Nb64Ug!$qz%dM3}M9p_l62kdZ-hOKxY_ILU1~YF=OzSgs&b zFUm#L&ye3-+#+~f-1>ob-BFlaRK@58#{;x@)C@u7M6_+67iB(_^f^k(h%-u1yip1R zT1y$Lf^Z26`v!^hyVkuJxP^(iF^c6{O99mNd&0+%)T|#HqkUL^h1AaoZj91y%9?L+ zT&dqmt$$8O-}Fy5j=1oxV0+gAFS4<&J> zAFBea!@7e~i!sTUB141>p98}=DPKL7mhEG16vtd2q$_M-Og$Ew?a`wd-B+R+WAq0& zVe$`CrcA)XV1iy4)YT-&ZbLnms_n&$VKFJ9IWndmi`e$)v5f8~wJ^pQz*@*2tup@v z&S4BhXgwCJ?K#IWXMf4b7-JA~lE<5Hd={I$n#l)9a>f{gk=y{s!6rt69K1DlLAD4!8rK%#9E3N@HYj;uTCZp)@~Ojr4fhl2v5)wYDFPVQ5+r#LiM2@`so&pk~TY zH`8V@tsbxb6fOD7H$>V@*>WwM)7BjPDW5jioqoXZj|VTpBx><0vIm>a;8GwQb+Pa& ztZiWK9kLaPgY$_4E`0=d(p5o{z-eWlF$%~{a(c^LnfP*sQHdaH0cr%?VJKB`-qQ8K zeO7*NpbFvMbfAAA+KU-)B>d8wk!RE^W#fp|L!pB9g}%hfiYQdjXi5&>0EqB3A%dV& z(Xw?Vb3vy%4h%5?TZiQwiV)77$OyJ0r zmS{LEy-YUmTEu#caj3gkY5mNjesMWEnezf0VZ*gk-He7@LOHHY^d@=)yPg)R{y1_ zP1_>+HS2a66Byl-57N6Epw2~gypAqoBIEEy0&7=iq%#EV4Z`^(MDQ>#0-*RHV6qt? z>lI&MOl}suL60HcppXD>L!h64(4oBOv#LDfDtLT429J!l-POUVfQwO{ne-PDbc9El zfg@Mg`@xp~6-oGWkCB_#E=IG%&~Ct&nXh&V+5xu6kjyZfhBpae%AtVr z_XU3X3rh3kl?9&rxO|A-Q0S0p-8LFT!Wl?dcBnBOSC5nNm5^+#*|yhm zNvnt{DK>-mqs%D2T21Q-{grDQ^-%il?ODY3%n+G|F`gzj&J(+9X95tsUaB(=3SFb`RW zVg<;e=gU!rb(f`iI$Z_d%WJi2NMt~0q-yPKaxWqJ=c5{<8n`mX!{FlufdvT4?sJlf zgr-P)Sq!2VB03`xw3TQqqBF@%7(-i0CT%ef5-vu<2yG=cWq%N%Ej!521WY)a+)G;l z4xgxP23-Cst1|>RUkl){tk#n7lTQ>l9KnWVOCSk4gHsr(W zrwH(bUO+X*1P;Pw>~=0(E@!vuLCCyZ0_{Y*4yRgW33MU{;a{W>MsNv~c2m|z)*xg) zl(YgRp|74Nn+U9gr`5$zT}gbnGcuW#VAWv~D&e<%7|JCF+O{cCYG}UrDJ2P*F^iwk z;7E6w@=S_)^V7(D$mW?i>{)vfD7B#rLL!qN*56j43DBeYR!t$Ae>w1Lp1?Z^G5VaI zEGn&x$jRqosPJkG_^z)QC+>cSggKZw5wx16nL(UPZLy}aT{JW=>ST7jxpOkI2V|j@ z@MHiUuA$+u#~MTc3S%v7W)pb3w~Rl`8cr5a7G;ULh>JJIJ=+T$!A4A^dE7OaZp>?h<`Q16wEb#jS8(cGGmRUEyhBXN9MTU8kdssk$usjPqHR zC28)m(KlXL1LQ*L`q3Mo9!8|?>`0CoXy-U-C&XZEX`!^Dm`KRMKmx?w=8?;h6Ot|{ zluPr03z4&Sqa7IH3UsWXI9 z#x&ht`t+MsR(pr*CgBw~078N=7xx!y&fyF}AdCx;`1GAPD;@$kq(Xqc2~nh^_83E0 zo#(gFu|78gap3(BtH}Gu)m{b4PQumCSqKO-Bku3ahUE=Gpu~{LF7kSt+*gzSFG7KZ z$zg^k7}GhzE=EWgaurvUrA@T>J7Q9? zRESC8GJw~Z!4->WuyHv8!Pb={pL6v*weel+*m(-R!{*2{^+Uq!GOo~xxej^_=JKlr z&k*Z~=S8sa-O5qR&;ia6BrXu#LYP9kfh&=-G#%m`gKD?Hmvk7b1xP7rxaUQEV*S~G zq--!8<#bfRfn@?m*#tO-F=QqjW9e83M+F_L;D83{C|g%jkJG!h^08l_VnhDkB4l5S zkULt0Y;O^A5kg|yat15Ipw~Z_54nbT1oa`ttHjjYC9YoaJK63{aps+PIU_fH4BWjmLp? z%RJ#_NM*JRJ1Mi+vJYjnF@ifOv~8nJTBbjlsH>OF6FhiHP;^LMbIw_kG6Rk7Wsn^!wq)gMw`4b-++- zE1g6V7Px>_kRDV3S>T#Y(tA=!j}choqGh8z<}%l~ptcic3#Y7%XUX0A3N!aCOCe~Q zM<=8<%qtLwt?HmXKn;_VDh%bMLfdwKSt^T{clD_2$vhzi3A~smc0}kZ8jQUplddpN z@JC(jMRY#y8`|lIO?DYKMCs>2(APCWKM>7HKajX(o_H7qU?&)-O(Sdz(l=f)29ldS zu7~~J9wr@q7()%Q8T5y)W4~}6k|c7}t^jzmzFQa#?ZD8EFrdkKLO5_07rl6EKGY0p zo4b%fTG?U9&JY5IoJa6;hSs6^CTPj4qa>bSGJ#jn7|oRN5$!q_Bu^(hL(l`{D|om9 z+8$>Qv885SQYV*|9@dfrkwz{J=?4Wss}U|ehXjS?I}ANWkW16Hbu#mzq%t-vo}$O^ z5rC(6#Efmde8j7Cq}hszXhn2UhUqs`p_A7UWAo{7`M7xPjp*=m%o6Um;X&7ErX(`( zFcd*(j*0-zW4g8I#HBDBGuH_4hM{&?2*3{LTq0l*aF2JY5W%G|+O{Dj2=HprNkb8a z7Q>Bkb|5y!&Pur~g@>6FA`des#5~MQ82NgeY>N}_q{bN*3D(hXerZhuu3u}e zV`u5`#zH&}4LMK1jY&fa!gt$bh<7@ElE`q6?3*7#^PUF6uLRZE_sz)%Fpny9tE>x{ z@*?Z8@qr{YG*hWR2p>2LQJ@1Tir@odvXLKr^?&tmN(5ylxBmY(7L*|1pMzS$(5^Ug zp%%prUIXBr{9=YX9AP1!qZG!+`0t*ik5XHP2viE=ZpXnoS zLdTh{6p-=s1HlT+2hFystFU{>FrdtU#LtGwb1;xDOAenyoQ4>|X@$0Jz%iettF+zr zovpVWS{&QHnO1iAGFV`+^9p!sO6QB)NS5bte$z z1FfEoKTi0lIiT5R@#i_z=6^ilfvg4Xa%-PNCVo6T_#wJV3lP{skM=c&v?(XJZBj%` z5-{ONk_UQ6N)f>!a0?<^V4U)58yNTcPY~nMieSdc&@gUotscH=)J8owVte_7dRFkE zah4OzV|ABfD79!7pC4$Wx<3Dl>hgj;j$0jH4QQjfr&Isut!oz?a@^|pCO{k2&HP_f zmmQpV-0JugKpWM41paU$eW%q7#!MKA5TL(jsY9?vUw=_nh3QzR-w)sidHc&dAlkJI27`m_TXx7*C51k1rwRLcS zf#pZg^(LBY??iyeN|#}S6gMbp??OPKJqtN^@SkY9J&;r$e3$sx48|Y0o0)>~Yg$Qk z2?F)|?PoCMJxIATa4$?hZeWt!0{6kwj0c(@lWhYzrI21u1UZAT0Qnn@EilZFN|MxW zB)CrjQatw)9T)-o^hI2>lEglJ9kQ1qbVS4(*lH#nHQHk%gHQU#Orqz!{|~U5*!C!X zJ>kT#3Q2>lYV4JPO*$?#;0oQ>Xq7db?WiMOb{ceTR7WHH%es?&fLQ*yYIK+On#?wPhtc@XxPYAO$`m*47w z@{kfy9#ZVeL#Bq!gmW{}6HW?Fgp+m~a3;46=UJ4(LMH+z>-{Y_1vV2-ioArA(kbDj z-3FW~ZNvF@9OpeTI9cy+!71Xni1U&6VhA2ch}eW>%AyKGseLFK}r3P*fe3LIZe_q##qiY<*{k%ndU)B!x&=)(+rJGb8a*ZV~mwZW2}Oa z+#J1Uv_x+Masa&~+K6sekT!1&q$fe|O|bdLiFi}(!YONJ$_bo=Dq5m7t!=a>p?gjU zt+DE9aBEA@H)zmJT}i7}1^%8;tkQ?_um*^hNwi{ho=$ufElaUNVn3G0aF;qR@DOku zz8bNhF8@&I^lI=!Fz_fH!UBImY=%ESs!sX+p@$Ks-RDmb8FVp zoMh(6NOy(^h(EnGy$KA}-!}682D$cxkf)*Mwpw{P-?qrrpJ-=WxU(j0x*f6HS)&IF zNYpj0X@FQF+aj!FTeRC?Tj>#~Tb%~-1nLuEudKIiwiP0nxK*5)(=!3E3!wZi8GGM? z-kQYN`GlT!8_;L84Ly!g0uv_!{clfVA%Yo&UH~QZToSRMx286ZF+xwf4d^r5hJFXf z!3jbC+f!SJU`C-AKnXp0vIV_0;gRtYdfIJ3pVc<>@5%@~5#0awgeMc2K;O^VINzF4 zGQ;NlV)8+gr;y`8g|A+=aGWw#CqZ&~8sX4LB6ohUI<6`pd98sYZV%_)*851CjzB9M zN!3eG?6E_jZo=|KE^O=fAi^_|=LLjY=^1ws!hc2hCamJv!w*6k2Cmc?tf$$-*Fgi_ z2KZeWvEfLro=4d0_z!@a?Zvsbvw?2go_w4-1yj+rvc|d%QBa#9vT}`;Gt`~<|J(ep zBb(tThuz`rNTGL3ti9uF5z^WwjC>RlYL&#d*ieavMmAI+S0BWDfl2U>Eb>vZdM@ri zMhCZSABV#{Ye#sUeoq*iOc0H)ftiji+uvc@JPZgiE*ZfsTiQ)oKUymX%!iWpu}BmU zmt8v+J09UbAsB>s1Fh`vjj;H6hq2gh!chv8#`plP3HbuJ{G;!_;XaSGf2@H?2=8Qe z4GiwSttK$Z0t_Q~_l!M5E9b#wwA8LaYRx3b+n))6f}W1XqG++f*ItPivnoR zq)ZK}Bx}lIGte+L2@*Ih?a^(81n$Nhk37 zFXsa6yAInKGymD7}{{1c0wY z%2g!+v$X`8&T(1kSs=Ahz7F{tX}!jCNLp;&t0e#H4kih?I)LN&l1pOI8XE@G`2qq@4|v2c^B5Cz`IJvU=JXF;95@zXp9TIfDC~bVaQ?K18|rO zgqe6)_d(iqyJ7OZf^{Ug^F$Y91P|-dZpv!1#w_z;Xa68dO2)oD)^_wZ0`e$awNUK5 z=Krt2qRj=s$05x|R}8QYhbLVjESkp}>h3^5tI>}mk@?kl98Lg{O4f@J8u$c;Gju15 zI>grAN3!Ft@cnR@CtP%E-C>C0R%`LJbAYYAT9h$Ha95bNYzek^Qh<=wx{U`|r0fb~ z6^o1#LOXfG!?3U?uUCl|qUB%V56N8N+NH{uNkf>!eM}ybydmu-xS5@JOU=Lm)t2wA zbNfL9?FnNduz&`SKaD`Y3yeBLj3AGvZQIHgp!Iv}MNX+Ndre?1YH`+X;uLll%L~!$ z)Bqw2j}w0sE(5v%J^}$lE;+vp2hhktt077VKh7MQXu#-kimBxAWPykgWc0L~vLIyG z(3oKSgGI{yYD{TBT)Uxu;cZcX%tM)s4!nZYc>iDAufn|`T#aBIpuul)<6N1$_BBKr zufqT%6SBCJ#PSjQ4EHsgk&}?jB}foVI|Yi>xp3_?{)x;?TmJ?edYAD{xWW%aE3DIQ z5~%SqGV#;%wkwXcZy|kQ!R)}>ur)Or@4%^*Sld-i?!0I;CWiO2c#TF})eI7i9Qk;q z4iUVnN!zw#C>^V>YFcU+u|u^>uw{Ga<@@M-qHGo%40`PLpLQ9r^)^quVVCks2{^i2-M>6^Azt+*sB zYqR{MmwB#(J6F01DPahb7762%UbQ4)+=@j}jKCCHN82W^2nnZ{OS9ynoSU&Un;co1 zO|~!1awP=csrIMk^uyKxAx}EI6Fk;p!F3F3%5s9ySR;Zn5=R7W^G-dU*MtXogac=W zh1E%iHf8mPYT%T?t=)}d^=F}_f&Y_?dLZL-(TzZIV-9%t0OaMka3u^=QYgjnjPh<~3NhBa?D+~Bp^K7G13HDT6lX3Y>8u;i zkpO+jL=wouBH`gu*XtrpNGb*d-bZT#AHWb1;zKx0fr%}qS^#+u6c61&4wjuxioXHv zcZL{25rTG8R-85WF&~7Hk60v>9M*)xBBNlL4V}@lp^S~W4yrek&E^T59}~_`SSVG* zPc@u89HHU#h9PWPaN_*edcygGh&qhm`7hc{S?(A(y~bxO5*x$DLv64{io-;v*b zjwFQ*jN$cyl5hoFW?Sl8wuP~QFOkJOk{s>1`fjm>uV&%>q;SS)7@FMYG5B8yt~~-H zJd#nyS9I=&Q)8y}=T0d^X>=rnpJz7RZ4g=Tm`Tau&qQQl1Vt9ww&zZz+w^m%=6+Z3 zQCs5@V!9`xz7V->csiLd{|b?-uz@l1bfbPR(O1$BR~X{~eq>Yp1C(o>AoM3{c^F5# z69eyqsI_E_EVsc^w%>d)#D^C{eAbI0?5f&-6CqV_X|&Zsz3_!cz>nnwCTMb>Pnf<& zGUF_?-oRO(lEAlw1MfT--@#!Dc^YHM;TKq###pk`IENT}NTd@+VBV;s-ISFaLqN&K ze^{g_g@0uMy7szCxUJC~EIzfxRt!`WGUx-evcupX6c2j_^=Yiy?@=aJQrM0V+Z6c0 z7QjXuA-3j6gyLOU_;Uh~yUiU0&RpjCND5!Nc`0FNFjn(0bA{PFh=CZ#jNr^g+a`Po zL8R!<#wKCY(Vr^Na@u5#oHl9eY15zX*V88VHqB|10+g;iEqn+mP28qAje1r-Un*xr zMw+vZwr$#!%G1nglSty5oR~IKBhzN8ecCh;nHd(ehS(AH-C@`$6#9U*i^Le&MKW-T z9FMq|V|fyK$s$Xqlwr|4Zy9s!1%3j;H_yWis;Sh>cnvaIeT^IJMr_5Wu;SC$e+`V0 z`*JqcV3zT4Y#BXb%Q!V!24jv>Mq_Ll#j$0qiyTxa-$-lS;;mYV!3`M& zGKU%gyAC#%pCdoq$y0xV&w*LNE;I2_?70Z%4!J!&Wv0oHZ5H5fTc@F5V7pr#ulRcN zbs{tU4N>4v(1Iy5p?dWU{xzug0^zGrwDmV(k`u=G5JPA&V=JEcZHGU@y5rB9JK~SU zaMcC>lER#%lCHT)B?Wm&CGFcKmE^QfD#^%CDoO2-RFc>+sl-!|kc9i7c&a80T^T(} zvn>8_FMx)rp7r9nY%yPj$q!Pv1OFwN@%ls)9HtD!7VxMm7$w-(-(m8oE0}A^;r~h# z7{Q~iw41V^-D~}y`B2iYERw5E^*s?_V@5%eJXk(Ro-qdG*OK~Pv1w|fX&9r^^o~t) zI@36$9LA7Yml#-vED2+IfO8S8FqC{wGH=2f{584(FP<_a#DMKw26mx@Fcf-(F~j92 z=kytdcT6QfI_Y`L?7_1z^fcOJ$_~mou+8YET3p5V1)ulgtYE+5!wnCL*74V% zN`cLSObUM^l7CmL@f1f2%My!%!>a+Y7p2L^Wpt6A4lzXr_n`$}4r9Xuw9N>LGqjts zz`JcjGQ~(|k+KHxHsblU*HuOa!h%6SP>VHaC%F_7>J9U2A5uIPCC^O73jC>Am>~PT19zE)@!|~srWru@20MB0e zskjU9nVM+`n}b?^8G5x*zQEcdHgYG}nqx3i$pr*pCo*S{oA(F9JWZvlr4 zL%jtOwD}&~;i)iMC{kSJZ_-QXy#C6Pxnb}z?Yehia>HO_l+YN#4FlRXMT+@Q61H`@ zVW6vRa&IN4&O_fCo5-F&LLx2eTf3?ZafUvGY2=W`{>0^6PDeO)V9~w?DLxD}*a8XD z(M1FaL<$;X1g9g~w#Oa?B>h>ZBoH#I@>+F8WE<`vh}xjP=&9>+hd+(hg&l;8QCH(S zXNaNH{b}2D2C2xcb%yTHszF*(PNwdHOlh}9Cca=J6xO+|L>8e{<)eA{Q4+WU{mP>l zlnhMzVCR_h(a{9Fz5Qp_RK+oE{(I*XGFwzRcOA|Ot=PS;3Wj5h+ zEv`T=x*`&*-=C;Nz1aBSTL)k+zVKRk!@wCnRcu!nd+hQq#2T^dbhL2^5Qldm=rh{0 z3ad)IVUUMP@B%CVk^0OxMtHaO8;C>K3!Q(9SR{gCXstip1~{Gt9U2JrIS(2!ZA&Kofm(OSekD zqgnqYQj2iKzHD;e!=Z*Af^fwc1N&4ZVaPqazWF|Yjy^nTUf=u)ab_*Kr`c=K<$FT! zNmGf(Ciep@y&ZbYz`3}RdaTpf>%Nz<`G_rSV2p7Y(s1>yB+vnYvc%RA4s$Nk=y8W3 zdRpkg7z2Tq9R69vOGa>*(QeA>V9jNM9=B1zB4wBEXT+N=7>E``dyNr+PDp3dIB}AZ zEtY$xxw&~<8}jLceN704-T|?d@*<{3z1as))M)(C<0>5#cPA(g@0KD}a4BlDoxii) zv*(GLbck54ZSPbzV^~SMsP!qwvCNQ`>cERtf!!}`GqR1{ROXPA3cTs#Y?wB zmimcKZ}3nwqWOg?8(`emYXh+JL_j_Pz?>mLZf77j>=abB8~O+^G&mb50*)DGbYU#S zYm5|{Lw-U6jk*Hs1SFgERupobFz{y(k`{cu^00TL!7L`Qo3S7Aq`f!j{Cxx+ZM2#z zL2a}e=R*M3Z@1bcvNY6bY2hZo1WCU!#?=yml5NmeUO-fW1dWugKuY;rcqXAS8)qr~ z8dXi%1t4!zn2~f%Sqw1=Z6IoFozS?<)JDs&JAOhf!`6`8EF$r9jf-T*H7?qEjjOez zEL;W9mHWcbf-EY>QI;*F@&QsgBY2dBwr!1z`EaQT5(OALt8M$ruQ@L9j4YCx{V)qy zi46AWsJ87H1TR67f<)W!*^U5DIox-hlBHs-%w?b*<+zNG(b1Q z#;v58WX|6qMsQ-J-IUeY;{42ql6s&d2$z9k*!4%yGTZcbA4Xkm2_gy`7-L+8Lc>tk zP?*5A8=Y8Rig@EzxJp-=;PMgZPH1++y1vtE!QtJ|OCzld>|sDbKa-4jiRg<6q2Llx zB%x+XZXun(9mte)7%Y%S$s|b`strTE(KUqPPatqSSrRLIz)O=W4X!w z5c;Wh0BYqE_$An83t@R6PQI^^dl^>&3h_3P^{{#@|Q zlko2Z^q4%KHMopnx(t469qRDTb!)cHkWzS9l*y;(k|!(Y!{?bv@3654bGfBLN0^v$)bUr zhAep@_y7sJ3P5p|YRZ1rTCy|Gh&_(kp>`yq!w><;crbmD@ffhMZZ@!|J8$UCWF5{r zCWuid!i72u<|$^uG=(fV1AE|nH4Z6~<6~C97bmpx5ylM33QBZp%uPsLIv0=U;G7Fc zU7|x$WC*fj%X5TWx-fYiwNB!ICl@f=ppQDi#6?$Ly)HStQ8+Fms8-Ro@jB*1NjUWH z!{Hm&8W@H4JEdxIdec4n{S>eVoKf+WUkHNoc=`E3cQWi^*kz~N5Rw4yN05!t#52E4 zDu;`Bguxu_T+`G5d&8K+EP!!@;W`5RnlNWZ@CXBK+h7-fy?U_cgo!suu|B8YC#WRD zVI#_4#2(DRaxWN;FT!wqF${-L%J*F4(h)O&TjmIfSiqBN z2t0e7z|+>;vNhm2yR`v4W^xpD0tgJT+TQ@4gd7E4;3V)I5*F|rB>-N=g#E#w)<7qt zZv%85D?TyMS?O3>2vgi`fnv+$DG#0R1tW;&Y2UW(6W zM{=0LPMyTP1Ad;A?li_=*u%EH1RQ)EY}YLJ2vTu+$%ifP2go5_q<894D5^(K=W223 zFw%xuhbMG8APw~eTMr%^Z_@+qF!S&c9tc8{RSo{%;?J{kX5KzB=dp>pNuCgnO~nPq z0_eavGAyn?SHodyEkqc%Lm_2?PeGcN?NF>#mmKCwq%*__u0PXm$^tjkdoRp~lE$-0 zyx|#|z#{R{_9z;$mmwZZ@V>)%O2~q6ARzRF*d@q@p7jML(Kr`Kki|4vu+|$x8aQ_t z!l(rtxIPvmOAh1hFaXC0a(&vi#U=qxuUTuf*WVIBJM=$n012L>xC%@L2pUzkuhT@9 z&-Hdt8_-~L{Dh#v%L5);C;L6YCqX*a^c_Q*Hvk`ad7uz;1jeA$Efi$lMwhPgd-X6a zIDQgqxjiCpeF0D(0rEJ{_kx>p6)e%Q`Cihi_!pYtxNk;^u#Gjh!pg0>S;iWqe2z7U z_msEJwzVs`dX%kIbrAYn`68us0g1sGVgi#;m!o?`+ukkM$_=6+^DAu{({5}Fhv`5~e0;C7e@vC-hjBWRY4w)38djK*qh@0-Od z4=@MH!k!g`TH*r2C2^5n`)5iIki5aY;=*Tx)*=0zY+?vp#Tx0PKu-uK5aSY>+;<>P zbU(?YMu_g_#t|0@WF3CEd|rn&a=D?Bb{zy_S#AK;lr)~<3^9~z4YX~G@XTZ`!Y4Hj z)%Kgg!QUpY3_aBQS>ynGDD6T)&@Tmbise62tK zO8Joad6y8v9a;?uSnd+uNKTP~!ki&Sa9NtRjZ-im?h@jVg-<>#4MyFSZBWYlq%i}K zCYRvD5ZK>~3(SPC$aK$wJG)P=>6;B-s0K!YF$ZSZA+rcFn=is%a-o#z_ljloM*C&O;~!*{35B6~NQ_8Y^B^cBn3cX)Ik1c0 zm%XM>zTbC>8FuA}1yPqjVL?WmWW%RbI@#dL>6dN;%1q&pZ?%vNcR^S4`Bj_}2=*uC zJOv(`p5%82PonQ8;2tsWB}QUAA#63q#l)Us%NHASPBg~YkAjW)XlJ9Vs1e^!j?jaw z%Pc^|kVE{sah|^KCbS?lfU9jH#>o5E(cl(5F$?{X1u`kd-vxKT#uLjCh6|(i_fAP( zHlFBFye_ay=omTy=@`>3=(q?pJjE00j8AlhaKaFb4l^)JOuun~mW)n+Hj5>I(P3f* zAw$*>Mh6ACv0ue_s*g+-QXaK%xcTMev03G6O~ zUXjmmNxP&$`2N;P#!9b5gN;=PERM`mezcY|6R%gXPjIaoE5TRQzpYfTU^TOZc7} z;!!rnz!XGb3}Ei1eDZs&!&IU!XYGYtlgB&z_bw+r=vNkmTg`MR$<=K z^@K)6TQ?TY!m=*7XsU5Qv&p@Y%iyO$L@dm3hnWPz&vLX!&M}8EuDJfybM4^Q&`DVR zVt|}$4r43=djb>RLcj+n73R2nAY90FdYf;})@A z<1|F-c>#o^=Ay^HhRr#-`8hLOLsqyRtU4jyoEJ!mF5~x7gxopri9N*KGA1CaaZEt4 zz?jG#brmu>Yl9jmU`BBP9KiAgYHb000i&=ixrkrCBr*BpO5zQ9`{Hz4s<)YKy)EE< z2AqL_FjxhTtVHQ`UktrUt<1jDIFrd2BDtyR4+3=MXnLo(Sc3l=1w^;)qXlrsKGFzH z^qBS5+w1Oox+U&&Nx_UcBpQj0cv*<1TiYQv&XA&u=m*P*NzRZEFhIAMRAh$SMY??& z@Z#GI2;vM$+op~%A4=kxAbscs(hA6qrg0mG+S$N@aSn{eF;tu*c!T0R1JFlDdbHsN zbr*b#1ZCs~LddG!NrzJb2}kU7U5-aC-DxD5?Sn|+4%8uwY$%4|Fvq2CpDzq?$!Z@q z6mMtyZ~_tSV+1!8X*Xr{vY4B+&u7%5q{7g-EZ7@rpasp400zfFI!^c6gsR}9XqHS_ zfgn0X*6nN0ApLAWK6|c&6UucSa$(T{Z`&PqI2}pk%Pn%%r?)>ffSdn5`L<+P7mr+nGQ$E{h$!+W#d=e6 z=JLHOfAZxg_r22-(pSTzt1l)>E`i}s3Y?EN7NL=8Hn9C^!Oh15oCLzPXesDjlu-(` zE`pxm7s#hir9q^Rp(J~%^fT<^jub1+w_h)^(IjzYLy{*nTBe-bA`iY6c>yPy3($zd zjPx)g{OQJS(3Y`>LjVMZx7Dzdh;yW*M=tDZdh~~vqFQsjVJ8u54kd@Nsg9;Gf;)+{ zZTC2Z9{qBUGdUq6gFUsvpMjpr$TZu=3Co|U@8Dbrkk;P|HkXm*&noS2rg)1fvaB@h z;lM^Dv;sKM)^0*T;39@vJ(z(W+@{GlA#gEbOm}^eAR?!z9vkPFbk{^C-3G=OSQ9BJ zb7hw}a_nQdajbr@jr@@_5p8|UC?lH!nBXT04a zmm(?mE9<~M@cR~jEE6ld%A>}><~##S&X#-+`(K!U?CWazU=ZB=#_fPeIFfJ+>lwA6H@C%TG3b+Lvk6doAy|T*p?r$cdnf66 zGH~S#F#<|2Et`VHTu^$cU_k(5)%E?*Hru6~Gugb$fV9E}#u#^TMByZpFAUL@^4rjl ziLTtCH9*#2tc9U2#^?)bShSgpT=JroKVAk!x;t<=iW`Cft^Drw4{>xrQyKCM{4H^| zCWn#^9f246Z>G#vp`zj=^>)6SOrG z+41cggstBWLMhACp1@#GP?;w)=`eYP8MXvO%%+jVcrT z5CnPX+7sfDa-$2yr7b8?9(LQxMw#EQ0Tf(}g?@qI4Ao8+WAEm0PLbjn!NpkGO>^;4ncJluUqe2+r4Y&2d^<(Ai{FJbHUeWY~Fi3Ho_zZ!mi92P{!= z#X`+kV?XDzJIF8(&qU?66r=&~nm$z;5Bl)>tEeO@ct;DvEX=Q(*|2OoM~H8M5p}g! zqlv~g82VrrHLPwk-k*{u0dZ|M=rg z5QeZW9=Ix9aXjR8wIMI+L3Ry%oP?zn();*q8MbE3 zU4v+F$P`b`~UH0aE?s=*I)AtbG=Ydtjpv)IX4D2mZt1N3W`6*on|H!5d^> zZ654v@uvvEyr6io=`J_J7}ptp$`wit8X*?$&K?=Kc7|JR8$?qLrrB#?pXnLu>KWiM z>hXckm%G)cgN}@Lt5ISqF*}I)pqQr(WbAS&AyZ0gShTqct3w0&On0l26LXZr zt{TADLrCva{xSQJ_uDE--=F49k|yN+ye37Ry5zZnboKMZE6TIgg3(=JzP9MR1=;F7 zF^~4?3fPJg2=gB0?4byxxhUVQ; zS4Ds3eZsqTMv?k#;&6V=HdP{A3&O7^?vUc zX7^QdhgYrWtH!}BRy~%zFm0e(nDDOxw`w=3dU=N9cayPSpx#@!e^$8)t-5xUTWy*` zXwMV#vf-cCl&igi8NL`$xz#-r7IrLFGYejr=2o2rCuabMebu%lRd`E#)8w@?`l?x| z%dHNKVfyjNHC$a?ac;$M_2{^BE8Ob3er!?F$jwzFf%)4i#;VSfr_P_C7WO*7;}rGI z)Z2T{P-jf|X7UVmR?Rn)Yn0o2{gh&L-%8dvL$HzW{a3+!b@s3y{l#iMAQ`AeN{hAv zTDQ6ZeKA(mEuK2xtsb1XcE(~=wPbCzTkR{Gh<-c=^e|8@MhR#O`e?EGpy%4^V=LFFelUww{a`hEo!T^T)97N=y)S#=?y~)}f_RyA?-I9q z3AGffpE|xU4a{_0pXur9>q%FXpQEmybQ0Qr_sm_>hO3nYyQcM2FOA$&GhE%+d)Ksc zR7u52z~qPM(|Xl&9C394Fm|4rIAze3^VFq_2TgIS{?e`k$^R-CsJtovD%c2&?OAk* z`gp>_9XG0(OWyz_htThCm4*^7R}~BTOb0!zcwyRBRpGt2dYy95A2IeC)p1GAlI`j) z@1g0}tAdeJ=NGFF zkG;;3JPrIz%;Bovm|fF$sg+Ag%W70SdI$3RoK^GhQXkJF{;$O-+o=kM=Z`H`smm0` z-6XW4SY5Gj+pMkXspNaBi&dY>Wi>(dS@PZ`J5{1Tf9!qg-@U%Y_5xl6s$v$PCQA^airV9#Ob(|*JHOy8>RK$(lx_vptOb^!QFSW^Vr2N7bO-yMdEFy+4@ssJeXu-L6%q&6%LiUw(ea3F?xi=L7RQyKSx- zs8R&m6NZvLFU08fsdVs-eQNJqnzJYFADykfTsC82wt9aV`NOd(G(QzCvA6Owz`Q_8 zxD?d&w7O;P73Jw_Z{-!`&#Is1oLTNuSI*f#x=1a+NOvnoZ`Shi3XbB?BGUFT;ZDEz zZLT^8ke}3Xt9qcmU=Z{TS?s->$hMT6|H1Be?3zX(o&tW{e)oFcb zP5})Rt4mk0v|rJ8ZgtV9%~da|vYLl67A{`0x$2O5dd~Fff2b{e-tG4fwY|?+q~A07 zfqAd1+b6H<`Z{9Qb;a1nC_CVK8SH)!LT)VRs3r~WxY(`cV$AMTMSx_d>Q$aU_ARw{ z+4DoYsESF?4}D90K9}zM5#m$#qov?cxoqQeSv2GPG@Cp$H;Vf|8FatYesfh{b;=@g zjLdX~4DHFbZo2iLiM~roU7swz9%FOi+(A>;s9#5qt(>6}%7^zlq~0Ghy?VYn zZ|;x&8ufGm`PHr9z;~&gm7mwlM_oVqKTy@vCYDb?&ywDT^gA;8JayV=j`8cp9RY7C znmPjYE}FvDC4-9Asei5*F?NA-(0rx96BaUM9jL{v{DOfO=ac8$i;+8DJp)d-6Se1$ z{X}){|3#lqRQo9(OvWrzUcDSVq=K9wsc)a@U#Rt6CYC>>QsyogTCA=G2P;;efb;KC zf37So+okU9Qd;(q>W$ck)YeIjtepGcIHljnZD#R#rt1~)~s-In`D2%Ay9cYTIgTp9q=in?{#P=1%mI~!jiAa#9 z-p!y7qfUHBC1ncV+d$ka5Tq(prs~F`)T_uhUA5;*1blx*KKlCL1X-F&sXWl30=|CA zfw;as4|G@rA5N=4MhaFjt^httOua{I-(c|_8S0wOxN`CBSV&)2HA;Nv^r5c^FS0S; z)l2E?rN)czol5%ps7c~Wl9WU6-EpRTWCr7gs~O_kHiEv9c*BHoSB#@?j9MVRKS`P6 z)MD{XTg15WYMJo%ou{>079NFTSKQ z`WCCx#n($QvJ5`-4zWZj=cZRw^=M%`zR-42(W)|bZ>JssF6)ZL2tM}5P zk&HVZ-`u5duwY^nBu4tCk7e8?cz=by8^yO%y(GSC#CMzer}!=v-`(mn@vRfzz3Pbg zG6t}ezo?(Y_mn{Pm~x?`)6`djvwb+tMBjZ9_Y}URN8cog+pm(v_l7|I9QNcH_vHeX zc>u4V)7M4ta#(c|U(E`}eW1FD?}bU3Gb+?as=LG0IZb_7K#KeXUu}1R>fQvcRj4l^ z6Vq3MRj=LZUuu9Y<=1ME&6nnwD=F7NM*y`tR*EmqLtlX-D87ZD*$O~%y7<S3dd*9bxf}g2WHnb(|}{It;D~v|xkye$J%NaGWo` z*P#uGq0jeE#-~D|K8VzK8PZD|2iX-__EFo{r1KcMZnvZq>_irTA_^ zDZABB$5x#WTfw_kmE&sh-G*_y8#`gwi0^f@cDGvSxK@16V1(~hD;?WyI9u!Zlf+@G z3$>r_*d@M;1ZQVE{w%)s;5NI}IgUHUry;x0akuzh6v!@d+%GL^d#eL{_UgGuvpS#s3j+eyu zv|#Co z&dzVer!i6F{Ew~7{>~r7r(r2~{%9*@s`F=w!&Wxxo9%QuN#(rrQlV;`apKdxve4-j z-zhlwvRf^3`oyPuqGdgn4cBV+2UrMQSri%~f<4|UUGfRANpyb_ZgEL2bnnEsh z=GjWw;cPE)8fP~+JBm;D*p1Fkwv@L!@$v$oen|TE4rg~;+KRIXEN{MsLviZEO8u2;0 z5EDMvTw4vPuK5zD+n(-PY|EGFS|&c-+I-gvTfQ!?RT76qFyMNaYlHZD$~YS1x=?%t zGRDTaHi>VM#Eo}dEIux-Rj7%sOU1WKe3M+4iElsD`wBJ1b%pr2qEMlxy0(Z955&PY z&2^Rd&IIq-t!B8kiEk=+&u%r(^#}2Vv5dJ}EqDDbGF>{e@B*NJZ*c-?M%gJOsH zYJ@_zxo!}j4}592y4`h?`1(q|CtNp+FAp4gxB8pwR`ES6^x5dT&FREAx_2=l`^>f5 zhNUlDcS%Y;a=voiFTVZKhHqRCicgQDZ(V=UDPJf2&72hAlH#+V)@jW2E32`roZfD&g!aOeGu~5)PXR!Oc-j*H6r5HxsFX5k2Ja*UBb&# zW*~$m)HGbz<>;^|3=0r`y3{yn$x66cLNxZ|m{=>^swHSk8M3)Au(^XMVc@jOY$01C zW%idgu90@JcB}kjlddz$! z!tFR`A6b}>;#`+Mz2Ro`GW+iJX1PwRr`|P7jzar6LQY@uEzEuskJ%vqFGBu`9iFt#VBYR9i$_u*-JmUVlxR;9i4sq`*VVbkWtdY`E zr8GzPqr=cfG2iJycb>Q(?LnAtDxtYi(&v^C<^`opQ&l=$@U{SEl$NG91p4lC5ud9} z3`au(5u6{JbT)FuQuz6Te^Onvv_9&*6_+7=eA!l*_R;;ols_OObIwgLe_p%`=CMh8 zVE$*`12Au>dIaYELcN+w^*G%>Wgo%^41XTx!!r+>@c32Stc(b^IX3Ct2%IlZ`8-n2 zrOUsKgugLrOqv5V9vePTEz18d!tZoAE3;K!w+Cjs^fZ|7r*yzt#>}3@!`d?Podc^o#w7gnj5ld-jGZ4wstg^<U^XtM<9vN}J>6Z$a!^1Fnmt)amW!O35`% z%)6_ZSDT}|ypJ-i9=Q=E=#Wu;*r%bSZ}33Y(bPSa4s}+>-7tU2cmU?1EYf^RuEcu& zf{-oAk0Y;D&Qox&t@@_DTUC#I0kJnEy#n*7=M7sL_WpsI_h5cGd`y~KjqS=BcX&QR z%KYTdVQx$N3bFl{GkinFw+NY(cns!MsWiV&|1ZpSSv1GwFwIeqbA&^M^O9h0N%q5B z-JTGBmQV9wT6=`-%XkCnpUnD83rGqPd(_ioM6?we!hQ((VHhFoR6(lZdWON= zl03Fq`i6eQ?SYyp2-lDw^~{izj|+t?7?G_4sY_v2v|kJJQgsH*3*sAKcF)=h)0uMv z%rS9$VAgu}pqv9W55i=ElLlUZ+rqqtIhtO>u)}jmV17j)`2gm@ zv?DP0WgLU4vGL3ZLOy?irwXfJvyxzTnmq-hbzO&ixb5ZZlm}`G5N?&<6>g20hD35j zUz$-xEU~V9KWXNd4ARV&IA#>pGOR65%Pi8j>||$BxHnX$NZ7qZU#r zYdl+0{zWMBysB*VnR8P!7&L?jYPKL;!=rI>6r-LTZyR#C;b?b&Y2dn?@Q!9&=0 zccYg6N%zA%>UmgdY~E6)!9GfFrax6U&*h1m(S!G-(tI_YoM&>@)5u$s^Bl}Ic`w17 z(Y~NN%X|&)SK7Y|b4xN?bTI8hnENtF8&76&)W4cTsw?ck^e^R+_VhTlW7`_JN04%d z=O|3}X=^=lpyqqTUL+LQvM<<=8vhRUt4I{z_h7v*7m-T0NJy#3Az3|sLQ0(`Wc&(u zk?}reL!JIMkuk3AvCl<3$Bo3C6>Tg85cHX`rwJvG=|r zgeP%V-_vl$&X^qOn)_Mhv!|lv z$CkXqa|7zqeK%vep5w-%zqTZEraPE+%Sh73U1;N1j{9LQcHfWm$u&>HbcoqC`5@eX zN~5W3vFf$A>pf}LM@Z93yM%|cEm52h=9P0kM|#ZQFbSL0%S#p#&NCB^qRifDq?4f; z4B3)JbA8T_2p^C~_r~^Tp$Ct8Vq4@upAH+rb~T|zZ@FEgn$HfbUC$SqBOR#88^wO< zh?M;oY0l=961tAcR+TOT=D*!E-}FsJ8Vdt@jI2h8?#IuQPDMFKJ)1_c_Un=UT-VLw zehBW*oR7i$B=cFAPi4Oh^WEG=n4h+zS?QVws9Mf1nyZAWKSwP~mwnf)_T!~LP~#lU z7J0Cz_}EzT|GO5_46LBJX9X#FWDVVa9YXVg5j2BSNnLLxaSqb+(UxS+@(0tBP)=;C zvk~%oCCiynrZLuht~^lF9%+bSmevXIa8@N|im@yFnUfOice%ab{-s?x%t{xfmw&s* z!2PCg8qA)l3t;{(eI3l3@*7}|QJ2D;6Mr2_Zkl!{%)qdRVGgQ!2BrlerN?*1alC8! zGnU6{Xthj5S+3p77n6wM<3EuQQg!_qwX z0KA0b;GS&~z?!97+=_-iI}-6$no=)4i`eYvdV z*>((X$Y*I?)EvNnpyp1PRdeox$=12miwoF_o2Ea3kfqC>gL!_h*I@oQ^Glew#vg_G z_F(EbM-$57uC5>l{&kVe9X9tLx;e3D{PEIMn7Wpi7iPlUX&FOS&&-1vi0cZ|!XdsR zYlfdQvme~dMebBFR`*>r2(^NUgL))IMlO=obP^3rL@?e&bEG;b8pV7MU98DR>RbC;~&~H*8$O$?p92BT|Cn~9M6&1(Wf!M+Bb^4eYwcn z{W2Mwlg-$ycBx3eCHX9v9F-1L(T=f&`R4%=t4FSmzLWZIM#$ z1*v;b&QZ@pFk`X7@GZ%da#M1jg!^-mz~eg{f_tx`tQgA^-j3Ax@lw{PsnM$U2+L3Y z2IlC*A7ECcvF%#hU6jQ!-aki=^j{FWC7JSMN-o=_>wTrYdmL+I@9b0=aPP~=g-NZ7 zl4nc4%F$u@8VQi?0_k8tmH-HZ}Wnxy&LJ#Z(EqujzW z-Rg@04ot)5~Qm1)d)}3~E9tE_o$=F$)_5|D(hkpU?Wf`PlJKiXLu?OEk?CtG8 zfVnTj-bbIJ-UBs+=lhOFVZOSQ5L(poJwgblL*1D8v()9ScBoD%=`ghhx-lyk?$tS@ zqAkfC;69i}8jhtIT`y_o$t-*AZ0UiT?l|cc24;jF;I{C&~&Q))iOlzHVEPP=?q4Vd-1 z#DN;U^58_u*j98!+sPrL;WLoyKutH$0Lyf%n-}So7JJDW1KZKw*t~O)*W%k8Q#(A% z0P|7LNife$I1A=msnj21_3`tN%i`P2_1`3Nu#Z#82cAyX(kbbAOY&xva4@YDy>D-Y zo_!9~tV54}8S-M0L!D;&uIj%Rac5~? zZtk>qIzyWl-?Pe?^K`XGt;7x)UaeQ(lvT!i^~ZeR%N+VQBaq6AefK3zkSy2tdfhGgZ%K_=G68j{uZ;@jx`aqI?X zirQuR26b_JQ`B9i@8QK;yQHXxOy94AAFfVO`?L@B-CvZVJ`$g`tEn?h8?`>5ZoSAC7>HBf4+ncF&n!Z6@wsy%>drV*TkcX=?)qSRK|Kb;mGS#D| zFEnH?;-1z%!EUBHCO!>IJ9S6*C@k$&wV{24rM;SqYbk`KwC@IIzFKPf`VDb=^VMq8 z=PB6QC10Ip`f316zB00-^qX_UtMJSKA-U%d{=58AY0NSU;SNt8nUkH z#^NYs-PEj-C}e_6yjQRh`L1*d`rv!kaYL_{yGwH-aZ;ce_fmC9Wa>l9e#YenM8b3}Y4PwfT zsD1wU@oK90wn)nH>J%M^l*T`Vg#D@L%=sPsUod5K9ZExK5CF+IhER+gm}S(X)(cFK-j zEKlW0>wTW@-W>4M@BN%V-p}WKU;M1^{(hh5S3R){efZ8?9;sSw4QqDVvTLU9txE9MW0%)MSXGSPzg_n1DpOT|iEogsCTcu7;( zqnpK!F>N^q<=!mzLvcUTbC-$k{ECIpM(I}Z2o(2oa_+5S4-`jE&s{03X+S~^5}#TW|uPqIT)iuG|3qpzCs zthfl@deOJ+`YC(FvPtY+FM{Szj(T1gC$sVjpW~J(^ej7+*1}#9oWs6au`}Y%DSJf> z6z@aLlzn2crl`HfDb3<2lvi{qZl3anSU9CU=OJ+bO3QgzxboP?S>Bm)Sd>F?uTM^S zS2RIsk?)IKde=jXJUiumu~1VK`TLaP;yEZS@bIZXIb7kFR+6rA1y4O`6ZTGv$c5Al}r~vngSu_$hd5&w6pf5qj$Mgsws+hLQS0 zSH<~9#BbsoU0sT1sLFjDjo4f^s+>=E&Y7@{k4uP{>loH+Nh2jvuUoURfmr;zk$I(%1`AE9TW z3v}fz4SS1*sfB$d2MSL=Jx?;Yh0x_YSBC25ei>T0p)W3y3K>1w6t zk~GR1T^*bFvDqZ+boKefOVT8_>S}+=$7ZwKp{w^(E=jX|N>|6n9}yP$g08+8A4cj` zT|Hm=G2H^bp{q|yFG;IBsw-E*Op8r^q^pvI+B};K=&B$2g5()pjUZo;{83l=GjFs8 z%YW-?_RNR!f~8@eHfA5pY{(0d!Mgf<=HsLybrn5xcU~tMqpO6OFOur3tL@WX$qSW( zb@kM=1Efai>Wi$m^E%71x;mG2jMOAuRTbW74U_r0swsRZFI>*n6rJ;u{>shwy6?!C zBcy)O)yXko{1S`UGJzZqDuDtO_=vn7zUELQSMk-!cC+N)TDhKN7 z>)0?-!*$iQ*AZH+EJIg)=&b7|vo(dDMae4N7oNV)6eaJ`ReX9{RFtgKm7&iO(OquQ zRcxOyQag0DgX%@gCw28a)r*!d=&FM1^^mXV>UOHvL%yM_HB_&sJff=&RIjJ}NLM4N zUW{zjm5b`d$TPZHF=L-8R-V(<{WH>{V&%VeHEjAmQ!lCJYvVa}dRkO38LTPnZEu;X z`;_AdJ+Ymss}M&RsR_D@8hV7D-=3-~`_M2_Gc|=QcDYpdy%ImpWS1*-btv8>>~giP z%n9R6@$z0>g(sLqynIksJ2J+Z667{rJ)2eKq zvPoC#GA_x!@{q1>D*D*mPaf0N>Y_`spZr);=yiYjh3@-~uGIeW8(pcaFj7D1DsJ*j z%K-VCu7*sm%^M(%^qeKnQuEy7Oo=i?R~2p(J$2qyR~P1els8bu>dIVvic~*Mq4hyB z|3B*cyg_my)OvAC+V6RTWuxY!d478C5c(xIx2MhX$#M}Ce|A`=Cd*}-qR6>9De}1v zkty=s|3r3~nj%{}L=KZZ-R(Jt$&pa_jGH=47C>otEUB`wL*z)=-NW^~;+>!&Q%A}& zD6ThdYPzh{6h56YznVH%hE!;gxoMZC7R!aY z`okUPa?1m{3Q37`mC3j(I8y8974qpTS#dvyyRMKenxcNL%DGA=FJzxrl*dkWT_fwD zw0bwlOHf?zYS#@iWD&ApByNt=acuXn<|+I5Q@ zd6kys{!!~)%jE=J8OCgMt&nbAna8Yjt&|N=S_`+yXEYzRKu^xdm!PV|3v#>bHreB9 z&RHcg%t4`d$WglbCiDqcrM#r8gor(^RWfujT1S>D*$t{nWOe$xt4j8P3dnJ#^gCU7 z`WlW5$b!@)Qa?gzeOWDkhvIReXZIxi^=ta663w#NwOU5&YO=Z6b*H4iJPqHALCvna zCx=E zSMJu;RWZ%3`{V&e0a{qjRyIZVy2b@CfsJ!xxpt(TW{l}3H}n+&^7YcD0d*;OkO zbQMm0sgv|)R?(M#b!~P%Aait;Kz(^o7VGNw=w{ajd7ZAl?%C|xC|Bv~7wXF<`GBt8 zrM^5Qcj_vY#$~hIr>ia0mo4<{Tc&w#tC6a;Puc#TCUf6mQQ`Dmpybf zoyNr{2kR<>#^n*|(A5+=M%(3dDD7xA$OTZ^(QJ?l^~lF#-f}g_yLFW>-g51bJ9XtT zz2$mTzNxEsZEv|AlV9uVs>q%K$A)2^pvovyyJoOV4eTXp5?blUZd?0$pR-uAH5 zuH7#DKqY1eb|C0*4-)%M&YlWx@Nt&Ohj`Mhk@)t;WUJztPH3Kr(OGGatGgjIZF3s zcMA&LFBj`-T1-&r-{sa0k*~-dy6^qapwL(3a~*uI%6+=;yYQgUSLLA&z9xB0_tkYj z?rM^jo7;Qlmtjy|LJ;59)QwjzPIJd72FqX=6hS#=!$2> z!?N+#cHd#SUsvpVM^>(H_q`+6>56^t$^mulzIWv)U9sY|tYh+bs4B6n^N+4$GJGS~s}d7?UU0o9V|4WZEp+<6jMvqDCQE*c z9H^`0u3htw%VD|-jymo7P>$D?%h)UbBT2vLK+ctxUil~FG$^h0k7cp$ySq!@{Ey{M zU4_RE%l}0Fq^q^jr(K`QkWE?^SEq6LpUGKJT9#J1K=)m+P0nwX%b>KQ5|GK;xSn=Y z0&*;rR__Zr5vocYGEUC_LKf@lE6ek?Q}SV5xjNuJ^Ot@>+YnpJk)2*!PRuPOk7^y5>jmg~B<$|1|tBILIqAtiHUESCFtDt|&VqIW`c1}f@)}*G(Qh(-lQ-+?k5T(f7v=4`veGxci}EgA-I?aeza;P1)w;ARNo~{= z?xlX0^}3IH{kz<$EAI90a<{H}44Y&9L+;g8!mt;5{UKk|)f>siP@&$^RZH><=|a7) zE8bhFKG7BLtyHIV<;prjZ*`v4RSA7tQtDs2>QDa{Vo;ZKm6;Jn%KSQywRXgeDilgP zUyQ29WxhU8RU+PUZN5?cql3?+e$ah~jJM>QRQ4O%@#CXuQO0-LkETUM>WY0<74mMo z&#Gc{#Xg(5yQSS{Qx8IEJquEgKxw@WQjJhm!ey+=4^kiKYE-`#Im1+u?)%30NPd`F*ufXBuGf9f&@2_Ms&(})%~BES zO$xKhEuGIU`jZRF(M35)>M#lA*L~tgBl3IY(-ck2dL5&Vo^9`0jB3>t_c~VX|Eb*m&*wr>DEsI?}3RNXa4DaXL z)ziBA)%<>bf_h0;Uj&`c@2d_&Y5Uht?f-Xs3;onvP+AN9)p01Th5qU@s48){A-JHw zI@=*~fcmvVWR<{8KgH#_~ece5!AW4nT)ek*V z3I?mmx|(b_<{F}=Kxus$s;28c{)`){mgtH<^!qzoXVsB*XNPHKBA()p*HC1@t7M6oa#|s<%<;s95LaIcuWo3MQ!HAZ>4X-zTb=PVM_XQ6=e$eUsFi{Kp#f@tmYS(iQtA ztJE&-zR4jSGj*8S3`*PL!4!&HaXr17;-<(WQp-|dBOi|sS_#A8~n4)^= z>XbPoGf&w&L{3$Sx^H_}Naj>ET32^>3CVP+JSgp2$yZIhl%u{!`Rb^yxMu~*7}M@6 zP!UktSx~5YL1{fJRQ;f;#DM503JO)Su1*=wj+&-Cx=QYuQZQXD($#HZPeGAds;l_m zeFZbrN?nz9-cvA3-KDF|5qk<|tH0^0N7p?CbJSK{{j1xag1Ks^uC58)Q!r0Gr>p7V zhYE^Sldh`ddj<1VtFEl(_XWGwGrm8cGCEm)u?>S~huuE4E|bajvUEtf|v*Hu*K zUkXaq-MYG8{a#R}JiW9m1=P=SwOCh2BKH(js9SXPIQ8=iwMtiScHdWUrK-`@(H?sW z7O5w7wKnEZ!By&IT@4n83a(af=qk`^sV|yhfeZRd{Sn;dRQ< zTkF}Aro_S}>K-Vq-i@kO_g%2#}-sy^=EyGeba`}SBy6yBtM z>EOFr{h|A2cN$Z8v+5qlt!wQqQ*lsLVwKHRxJ=#J!FQ{w(tTHU+EZ|=s_o!gsW$6A zqiJ&fO7&a^-)(B2?t9<3-gTQg45gjdx2rz!+`2aN-L6t~#pm@MDukZ@Y4hEoVsyp6 zN=3hl{*SLx73+$9UR9LT?(?d0U9oSK;`epha;{Qu>WY0;YAHP>)#j^G)w*I|wffrC z?yFY6>WY136GFb+vEE5wS+Sp{qBBgpoR`Dcl*aRUbiVSN~dd`ZC{HDE<`PmA_V<*HuXH zyuy3bzjgJjp|r3@DG&EdJ16c{Ay9m0b#>vrYE}o|{c3^kYp^UUyk9MY(vIyqbzoup z{;g9jy5fCbuU@*g-M3!7sVnyVO|84G-S;56?D)zRDAeH&G)uGqIp zwRqcoo78DtvF{=Eb#=S%A@!@S*tc0db5FZ(vue^6`?jdm``dk6RJN|zw^cR%t=-2f zfYKAT!+Finy~Fq~wsdvUV(DU4Ca+8SYdWvjxgnKfmW`r8!4JLFPXF7WXdOC^??myJ zdpk{H?~Hi%zEi^4KBGLB$GlCol;B7MB8_4J#hApK6mJ%H{3-rs^l&8E4I;QV=bVf9 zMB?<`9C3^IcG`cpau@x7loEXhvwg8GEKG^6q0BG0b4=IJurPy&3TN*<$g^W6^WZ$L zHGK$k&GI!pkD>IK3wg}P&*ffSL3aAB!&tUVOkhiDY5Ry}(O4;ZznEjLc5=?iud5Ja5K|G!wWMfva5m1_v6YUIZF{G+ zdn>hT5Vuipjr2}a`}y{J8uwpoi_bu>;$W4t@II zylo#hEq~h)kV@+jiXFq!~jA8;w$1pa1uB zbwM%L<<<@Ko?ZK1@D-s%07qOq(>k`O?IT|W+E3eY0LHjs+-s#U8qfO_;;!twxHph@a>1In$NSe4BWl`Vb^+bi{)( z+k}XN=+#^Exb=n^+W2y2&GMzYy+3^ZD0(BN-SPzHK(@E_S*vxDvb9;_5TotUis_tT zA))po=`91cIH_-K@hOP^i1yK~IBG}g{~n`9b04Cx?>r`LXPPz+9Y?9-{&KB9N48nC&w4GV z*1E=H68IeBC%Nfu6`VzX9_|07w;a>fR>vN8tgB`2X!(C?rq#xQM?lfLNbUR5-qz)P z>v)ZH?5)QCJTh8*dp0RL)@q=87+PNo*l~ol_>RYjEp0vNc)YYDuAx@z_l)*P?Mm-> zT(nj+>}dZ#@s2&$_<8z#Z|jM6W_FxsxX*1~p4r;2ua1$nv252+o3;LzGhEw8p4n)% zE}Z8b_gd?lX3?%4ZJayyN89U;qxyg19rOR0Yqhj&|Ltwt_s21Z|2aR$x5eATKhO_z0$7PKOejQ-O};;(nhiY-x`>=_2gUh zQ|r(Fp80=^94Vfpm7<>%xm2c5$)hrrii^rLDn(RgQkg|%HkCP4=29u9QbJ_`6%UnC zDpyijNM#X~YpGmEHzlrOz=$bNP184Masa~2bcs*1!e-5oYk0228lKgH*YK<+uZxh^@T|rfp4Eca@T|rf zo;8Bk@T|cao;6s*vqtb5o;Bj{w1#I5*6^$myoP5T`do)T*P&O1W**@==7K5pa*^y_ zuv0dOlC=GVjqcY8Zy4Jm&tR<15r&CHW^>8+@|>s}_A{Xm_`u{##J`+Q$Ig7*^RrxT z-kSTfyny&Kg8oxMtudQM2PsMUI}^S^Z!??`sYS7>QM~L56V;D@MhVZtxJcD!UbCRY=l00w|(fKK!4 z6|Yb{zYpOwzf$2hIL-a$zi#l0F3E=tej3RZ!y3x-so@w!{$w~!{dB7Hgc-(*=+8y; z=OX%(X!u~}TH^^bzde#@;P*>7+d89B^7tF+Y30`mdHmPQM;D}<*2|m5j5D1xXB20d zQjD+8C^UsoTkF&X^M|RWrbwL^kFGIYFjtg3MKNWA_nD;Ssr|8B|bbPL;bCcvYs9rE+X^jlxVgd2I>&KN2ug;>rQZp%7Cd@Ny(&NzjlX)vSQ0J&OyZH5N&CKDMyF<3&Y6>W z1T00RQ!H!H`g+y9punLzJd#?q=`JJUi1HYY=XyEsd5)J&uQKEt0ElM=-TRn*eet##?z;Eg# z8u*=@L<7HllV~_T{u+wpH);|M{O(Mmf!~rzH1PW{C(QhQ$q6&RNpix>?~I%<^V=aO z%={h*#q?fjU81g}Hz?*>@EKlcxMA$`wnoG>BBl{BdtliE%N|(v!?GWi z{jeNBTLMy91zbZd`u7cZA);6G>Gpf+(P(N ziBmNQe!ksEBe0HEfM<*Z#sK>OlYptfOkg%J4_E{&29^UC1D66<0;_>Fz&hX-U<0rb zm}tB((HQEJo-sk8PV-e`IujOVM26bbbK}O*d^KhUt*(T~3+CU)rG#EEZzxX*O(e@r zgnYdx($UoLy&${#ad>s8U6}?~ho)GM4ho&f=?Nij+*nOrHwT&>{%I~_T zTb*gohNi1Cch@c z%)zjWh`Ff6D4v%JEn@sX!t5A<6w3?4em)%Fh&zbMC9}nlx(;vfks~0Er zkH|y2MQFDO?e0|#gXtbXT`Vq$C^jq@^|86w!1MfG^?iAHL^&+wupCm>dCMXWsm?PP zZzx(cS(BdSri26byt_mH(Eb99$u){F{gmlM{B$&bi$1@l8gr*_NKGkiU6aW|)Vz^pCZvdOZqTb^-gDG>4tYEmPD27`M1U2615opUyA-OMgKj9apUN-(qc({HOj9Z&2Em8woAj0L^;iO z&1#97W0+?DBWfjTRioBLT+!92>oxp7IH|kW@X3N<-EB6zeN=b8`>LUb%y^<+sS>{2u)x(4mm zq1`&PxW;f7&4KF;1@o_pK4%_m-x-5$h`%&wH*g65>(1*jw zZyrKA^p2w6W)7SGfGMq(*v&gX9@Gjlw^RYPE(`)E9g zDQCXp$DZl7N6UZdX|tV=3yrZGo~cNsg@y;u9~*N_b;+C%lVj^z{t8((jc<-QWZ*OE z5bgsH8Te`BLk51fILCI9M)d?;gGXad;2JzZ*I;gEr)BZ*PO$;RmrI^`&eQnNCyOa@ zd~EDRj8&nnDQqNJX0X z+KDvrwX<72J$+)Y7_!q$3ws~flT3VvBaIiQ)%P-DtfGy4+%Fi;CO_6I1#^3}aVp&h zrl?)9X;CSPpR-O;b;P5Md{3LA_)Yt0qxN~qS5vf+zYV1*ekYb zw0GX#c0&V=e71?l;hg!GG-p)6II}z?E?`u{BjT3W__K+xsi4qs^+@U*YaYhuf_Za! zM%)GS*7=WgFVZa=%CC##<8^CXIPM3EVP8&1oYvX5Wfuk_mfN~H7=Lm<7`NQU`@Y=v z<|wwz$_qp|jU{PY<4&rj<9EfCV{8{=Y!_o}mzr$lj-Zt${w7sz;%`zmOYm5~dAVpx zear5(@rcz>izn@MXm^+KV8SK2#&#-wS$vI&S5>Mp@oGvnCSFBp3&rf~w#CHvq767| z%Q0e$jAPvc5*pF^9<;s(t?x(93-oz+G~odFVf3U0J=vmX_#l0pi9e}J`!pc)5@UMW zm3>Yke!cDcR8wERLWBDrQwz#Q^gWGM&Z3pGXl0Yp;aSl4JmN0`jr5(D=5}BWKEZu9 zQ(9BsUB<7=Uhf-e=Fx~T^Jw^ShVM2Go%?fNzj5{WKl=7Dj~m~)-!YXvA-!Lcna|Z! zGoQn$W*fXPA!Tm9F?oKW1#AyNFC`vA$n6;d6w%5>uX;*Ff^g2KUK+ zyKT<=K))h0f79{F7v}xYZ?DZA1M&LPBKn3vYS9D8)axQE(W40|3~PTKtO_1$c}M4!$XN}n0oDPx02_dfz&*hIzypK} z5^a_uhu_TKk?%~DK`)P=Lf?^thCiIR-gqhH$wXTaW^ZGzgT9p;E9}oDo--F$93*d} z`<=w_ATe14ryFh^`+Z^^X4*Q;u63AQ>oB|eWJJm6E{Dy04!6+NhGSz74N9_Tzpvomjri#IB>Zinq2l8~KHO{BFw3Q)4;RcU-QNwe8Teg( zn}OfHw;A}|{X!Gp3(G+hn8f_SEd z@2oN{{2eya!gp5Luw=uM4NG*;&*_e&JPV&!Mbw9V-HI(dw&m#0Vj7J)^HPk)F?A&W znWRDke-0KJ__MFj@DzQIC^YalwUww)fF{+!xx;o0wm8t&{r^Z>PcFzhheZPe}- z(0U8%o&=sIl%fc=ijcvhj+8YIJ#Be1eqXmn>~$mdx>4I}(>!}r@>%TBd1P)t=JUur z$6&TQk`I}B#$A(+q&G@w5X=ntI()2>X3vEnK~TYb>h zF*S77hLj}OQ(;eqJrnI-FxO0eKV>DZuWVTI2>T5?W@;>T3@ai|_pY$>z5NZt&RCC> zJv6Kw*%l+)Vr1Kd`FWG^fx>r(@u$E?!!{WUJeMfXzvKH3UuxxZb)}WB+-lUVLEReE ztwa7TzzgR4%lfCD!#S{s_B~9TGd&r9RceEkuhTPT{@z_^$xf?E4Oi9?M?@ns??L7y z3;)h-3DxR{T92phr*B64Qx8y!bo9~UVYIkB=rzwDsR3%mlol|3wV+d)H>fx+GRZv_MxME^5A%5=m~Xv@bXc4s8xho zMW|J5<7=(h#@AXv4I294s1xe>?6ad9F&8yrE^4H?$f+7J7x~qdg*T+<8Hdq*T^{aE z^Nb~9y(-(tdHnRK<#O`!{c5(6bNVgL>?2XT<%cwGCy?_H=Hx>LzQ0;8#iS?G4;lEK zy+a26Ms~=+@8O*{@*U#|w0;6J;R&>!XMJ>j`si|u=VEQ&Ej(u~#=cbJs^4vR#(9W7 z9X_EsC)>hvX|{#?pKal}v=C=zp^0bsLem0jvCyQ={9la!WpudZ=G5OuXCh~~`f*10 zj7>og%zu?)ZYYk+Sc=hGiFQ|_#Yijfb)=Q|w9uq|M)0RYk@fNTl8kB`*%NBOl>0Jv zoA_R9Kb=u@Uq`>ObgDvA?u-o?ndo6AdYFkG*66k3r+kp%lS}hLGI!g|)4OEWq4h0j zeG6JYi~nD^U|OD;nQ0Z*i@k(P#Vdr%=x-1!;S=8yJ`X%7z9;)h@n&X(xP-`O(kSSn!g!cqmx zYFK=*Y=>nBEPhy;VL1p(tGJbN24Fu0`-`fHwj5zCb7JS+HlpG6Cp;y$tp;Sjq{H zPpbg;!dnGz6)dZPKG?Uzz8#hwz$Wy;4{tNP&9EN?w!#~L_Y}ORVE+;*4A?sZ_RfI4 zGhpw4aq!yVO@KE6_P#(T>{+m9!7>3@21^-uIU(&A_#(qTp~|Y@t%7AWa1Gfzm2HQ8 zJ1jeZyU51dpU5wZgEs-* z1laom6Ukdv=72W~-YnQB0CUKDW0@P?GI+~iuK+G0?`>sOuvfuSO?Z6TYVbAWtt;CO z?{-*r0C$o7k+Npkn_)Q!JVf@!vQx01g5^u#8M6Pq%wWQPnXq3#n`xgoT$TWP0xW%j ziG+_9C6f1a*#uZ}h@UHSn|OT65MKs+1#l60yO&qOTLo_w?5lxm$eUPR5ASw(x5K^z zxQo1_%bVbBhPN5^gTO=N%`QI$`zcty1fC&$wCD_Z^UDop?3Wq)1+iMizouqVLM z7nn#$`$gU>%Cq3jf_(xo$Glm5>dqnW&E;-*-DaL!%D~GIQvqB=F_q<&=2~%bS{1xi zu&)NLA@4oq+s!;{?SN$$SvHh6!*URoLuA=eehQW^VL3yV=gWl!`(oj`CJXP8&9YA% zEKh)?FD!{<`LH|-mI<)rkmbj6x20C7>1D8&TQ-ZGr4{flBCnyMlDtEwSHZg)_BCXW zt*9sa4b!*7z617MWKXJShUFkEhscs$aSE0*#OGERtlUGJmBy+f0lbeDV@noqMHVbM z#A_ z7;I?WwoiOjkpP}Z{9=W}Rx7#`WxU?EGmP&lK7WJRp8r!&A?MYLonxw3$7IdW+Z@T z0n31uWS=#o3Vb`T8Q4npJ7=5%H-w-kKu1We*gYc)yex#zjVfS0*-y^c4to=E+stP0 zQ$Rx}&S31svr1g2S}}TN0xW%C$pS9}R+9JXnN{H1fz7~HvTvAq3fvHi{s0}Jwc^d0 zS>R>BDqua?FU;Hy-V8hi6rF3uz*&aQ$N<%W zWB@wIa&%S}cp0z?SRckms-C>PXK#n48F&gP!fQqTY(qHm0G+@rpa)n6^a87ZKHzqs zAJ_~G08ar$1h*?9YQ@^wGJ^9PfOcR4&FR%*e18xU4QMQ+7`@x%m0pKa1 z?84Wb?1Hv{ab4(onQaG806KwLKsV3>ECW^oy}&A9J)8Q2O808f$k z*~uc3dmwj6bO4<|H_!vD1bTt>Kp(IP=m)j}13=LY`GIjjJJ11i0^L9luoCD6)&qUO zCZHeK+KtbpR`38&MB(g;LR-K%pdIJ{I)QFrC9odY1Z)M0?wlbG=m5IAb7nVqC3q!x zJ+KMb3VSQKh(=GMkq77iRs!pRO~3=uJoB`I3tIS(+66j*ZeS&_9@q*LJrN0X0G&NI zi?7{o@Je7kunBmeCt3j)F{lf40Nub!U_G!2=#SwuHUJc{Tua8XB@WykyIGufJHVab zZt!}b50MRIf3CC%7C&*}IY9VoX)7!N#E4$#576EVIl-OaZg3BHCAb&79^41s1nviK z1rLCW-sp30&S?iaU~z)G!9C!W;9l^0a36RRxF5U~JOD1@kU5Ss$AR0yoj?z;GOm_Z zhW3Kj6EB?O1NQ?1Kw+;HtL8{MdI)p^-DH1ajtATe^Z}d5eteD}JOC8&_wcL|Un@*= zMLc>H&&S9ZkC_C#54eM3(&sv1@c_NRda_?N*9YE2e9K%vcq{SG<_5q;Lai7)PbT2# z6EB}<2X_#!pXUU36aRjm2fUK_kYX=*J@M;{ec(;RpDOl)2Y|8Mmc5nyr zf%BbxxGfLR3#=#m!udXMKQI84ebKAFwW4mm9o*TMb9#VYSbRXykH<>(LuQ~C=mRzo zK3de&kMH6Au*m)#X(Xh3gZ_Ny=>WQcl|V0I>cM^BP2jD-04!nv*OddXr{E5ta{%Xe zgL}X$!RvudKtJrQ-~n)vh-``2Yj6k935y%t174o^oLEy@p14^YF0F*!3wu4d54;K7 z58etc1|t7J^cLJcklS*AJINmGaf5qcuLSpk*Ms{I(**8Eo>p-CAmkauN5Kj12KNl& zoR#3!#J_gegZmKE1nviK1s6%2Ar9yOI+M^Va8DAqQV;F}Zvyv&w}J=2#bD$C+6VLg zI>6mP&tT3|3GM~22ls(DQ2fr)CRqHiw1Nl7-pwP1;7l9B8REe0;7*_$77w@=SPx4R z&_9HG5`bL{he$ zDv1Mk=vXV}mN?Tmvjkew$4HFQNFEu_NUmE6ix0dB+z&2Baiko@F?OH>7ALq5*a}NP^QLotXFA8b!9Cz! zV0}7gYXt_V1K=V9pEViC1MUDi zGtfG?2fUnOX#Rnv9_WL;3H$(Yy8i(Wz%DX5a~#kO^kj0*N^o!HW|8Ho2lv6=1l|e^ zz#_&V1JFK(>pH-l;BIiw7|c1~UfAown}B{;0zl(f?xABW#+EppA;8M9^qE!S1@{4) z#_|~Y!2>`sjxF|aJW395=Qw15#S5$-$C-WLP2hg;R`3Agg@a?_fKCTzaD#in>%o2C zO~mO_%uy>c7WiQa0Hu?Ua-0);;p7b2WI0^wcH+3hQV;F}_XEXvv_k*{B2f)Q7WSE3@3F+2yfOwI^dE$Wf9ApM} zg1f;hfnHcz!2{qjmp}F5a&dHWxvmrFhQ$N!1=hpj18<;OhfDq7t*{5cf$|>AKKX?Er^Ek#1bmVcZN}v}OAJ7kM&EvX(JiZD}6CN&=Q@L*3RJPl}9pG+Y zC9odYF!ebxtF&P%-(+zsvluLSpk*Ms}Oo520xt>6K0QGomfoZks_ z!{RQe756Rhz)}fIC0RaN;DyBpY|`o$a$S2NkF68DvXFCn!F^;Go(96hrA>vkBEjv4 zT}C(u*O~cQx0G&V&&m;AJ7jB0Ob{k2ReZspcm)^`hfwUypmh7Uy0rV zJwPwe2b2rBw|2tZQs+XRZ=J-iEA_zcCG6($E#wS-pl1F8+ynFieLz3Zehs`pFVF`J0G-z&572oXXZBnN3ornb*R#bA zbOJrsbEF?U0F+A*1M~v@KzRe~Hy|^(mw1-P4-CK}Z{!#!&;#@WeK(>NU;q~TQnok= z=_*^wpE#bST-OKg1NQ^%H}UTf>^C7Z&;#@X13-B*`UZ3YeK(^;U_kTUf;_h%56}bj z-olY`88QGpKrhe-^e^Ln27q!ob30*z$GMzqd4Tp6$PDxV{lLHqj+eK>3-sK|EqV#* zS^$?T(K^rx^a6b=ImS;&J-iJwKhSv_+68)nK41VSZ%11|FVGLP-+@{{?;V`eM@Taa zECHai5-~tOFaVTZ*nwUz>Vo^h13>#Kv%kzL| zE#`1od_X@?-h+6c6X*eYfj*%B9&R^q51)GhSY!>4xwnSv`hb350BFCL?H-^P=mYxi z<-3ajc&Yfi_(d3H51A+@$m?W{d|G}gJ!+<*)NqSoonfotD}yl7TS3Nr<2>V)#v6@y z8aEmnjL#VN8{agx7|$60Wt65)rf5@NQ;I3q^s4Do(;3re=5NeDo0X-Dv8L^R--M<7GoP=OSR3nt+3r`+iu%u`_OjE_Py;lTlb*BL2H8^40&KJUKBD!~J=<-aLce{Mt zWk|Iz+p{Plgl>>Jl_MZY8ciu!-w|GvbQ#LpAE44gUWg+ZN@x+M)tDoa|H^jgx0 z!8Z*)F!DU%4LRm>tqKhab47sJH>F-jx~hZrcvi$V0rcNwibQBEsPJWQ)h2w@+7 z2d&q(cUU!HSLYhSRhjn@{+eD(xFK~T;l47CxjuzsIw7XhB(}_mXUjV!TkHHS-n1oS6p*-<EIGr)f!I6E3GQJc4F=5vP#uqXe&%sic!TnrQ#Ars(hfU@_uXJ-v|KiUG zn^QT%^2{@Yn|psxcr$wV8QT3k?Wh05^dEMC_(#cq5Nh#DkkgUD8B)ivT~dFvvx!&W zGEw34O3|5RgAOJ%)7p1R1dGmuAtIcxljuSiA-WQF5mAJZwAKf$%}M8=5gabty;bt+C&h{;Y ziqX zgY<<~(qAL@5PyTNWLk-hP|@lq6~x~nRN^qL4nu!|hfs-kX@wa2t2%^A92Hj+KSrp; zd*T}6?-MF|-}pM>9}p_>A+53^#YconoS^3>>F>`ID)EW9iTFuEB|fDUW~BIxP>IiJ zy%Z^032A*taVzmJ2$eWRtItSrnox-^X$2Z7{z0h3*TPHu451R=h$`a$Bvj&CT9rnM z?+BInUff0e2SO#z(Rwemsvn^eKZ$#YpC?q}U*cZkKNBkYG`XMnuY`*J*6VuWzY!|> z`>nOaFA*yGo2(BI|ASD`DZYWYBvj(K+(i6CLM1+un~9$wRN`Z~mG~!wir)NxnE0oJ zN_-}L#6Ksb6-niG;sHY1U%7+$X+qjx`55tk5GwJN+(rCrLVlw53F6-nD*6jIPZB>% zNc%3ICjK2Eorh{S@y7}2JXFsSZzLQku2PAFi`5`H6XOhtgz<(!w31LC;<=(9@hM^e z@hM^;@jQ`4JWmWEK2;G*L{vNL)d@NGv2iQ`|s&rdUdR zmbjVtEU}FEY_Wp)Y_XE~9C17GIiiyIT(OEctvExxSkw?N7WWY^5gUn@h=+(T5L<{Z z5Zj1*L<4b;c$9dl*h#!pJWl*d@eT1S#aZGD#dpLPiXVtC5VZK8nqc5yB7 z?c#dkv~mye2C-W~t zwnerYo8KmbdIhBiO$^!+^lMN>@DIVEA^k#b4|y=;nGk=-k&sVAz7F{%#M-H6rvaVD zcPi*q+UeR(t2)(n+R2{X-uLwRGOrxuJ7W*fn7{h3yYJ5*8HRB|Iy9 zYWR$BZ@4e~@8Pe9r$nSj%!{}#Vs*qz5ls<)M3}lv?=rW`bzNTQaL}?>e&Uw5}_9?D?p?c2jxLOT zDY_~8V)VQot9rcMtm0_ejGc$*EPNBdbRXg-uvO+NpYod zcgDRC_e$I+aaY=}wI8vMkKYjgMErz=xe50q{E?8{C#TPEeH?u!_r1379ewNi?&$k| z-;;gK{U-Kv^?R`2;GE+>;bL;?g1+X+%w>%0bdX3p4dNeRO0W6 zK?Cm?c=y2127WiN>!5yvk_TlCT0Q9AK}QCi9u$+5k~Af0R?>AzbxGTj9!vUX(l1Fv zhKv|;&5$)iwhwuA$k8FAhB}8{IdtXFr-#~;6O&gYuS(vO{B`n8DYvKWNO>ydjg$*1 z;lpMOD;?H2?D=7XhNlicF#P@DzYgz~dPC~k)Q3`^OFf$US?ZT*azxmO*b!q#6pZ*_ z#AhSK$e58MMvfbK@5uK@eloIdRKuvZM@<+#b9DcVkr`KH9LR{x?3?M3KZ%IZQWz`g z2AJ+~+JA=7nuOMNDHdeDK`S%!damt13p1~tqp6NH-{{S@%j2RD*D-2I=GPe?+F$osEnjCioSED(>JcsR5GY!QW-;K zER}Io98{cC##70nl1*g-m5Ed)QJG97he|G$DOB=k=9x<0i#i>!@5$-w2mbxq-@!RF+b? zNyLepsoX+k8GVCXPGyCNr|%jGv>svt-ODA=JzO8Uck3%EMG}3#NTPeUB)Vs#zt>FP zCRd9gbZ<74zEcbpcT-tIWvxgS_t3Y?8Y=fvxsS^IRMt^hPvvh^YN^ywd4S4;R5noA zNM#e1hp23(vW3c4D%+?$EQX1CDn2TYP}xqUfyxg0{`#m$rMsE}x|b@Td#D1scbZQ3 zOjppo(qg(ts-b(M8oK9MPxnGTKJ;|X_0EylcFN)N(kQ;m;d93*-p}Fl(I^h)@VRaj zsX4TvZ{|we+Oner~?T3+nEB&?D_r+uC11j%}J+$3J+r6~iOWRjz`zmb@ z()J*2-=^)`v^_@KW3)X^+vBwTgtnj1ww1Q6wEYKd|3TZcv^`7PbF@82+h1t=3vDma z_7ZIkh7YK`FRZk+(zX+AJJB|Rwh^?AqHPpyV`&>p+j!c>)3!ft`_ndwwn?;2p=}Cn zN6>ZzZ8K<_L0boH9kiWD+ljQDLfa{{Eud|IVJm$%J0X@BKBw0 zLi}^_JMkdfR?&@0Je48;7jtg|9M^T73EplHY!DHTcg?2kTX?>M&-d~9;hHVkeDBumHhlK>?#f>59nIe8ec64X zcYpRPcz&&SAp76<9?ZVoJDD9^dop`BK6~&vxb{2lFg~AJJDHuv^XKuIS^Meibv!?Z z`p>T|Wd9k`UthbB{ga-10N*m6H?v<~dn=fBa24cy9}#pk7gt=a!};JaOaJTQ>`p9b#9e765@x-Ng{2VHOX-pc;rhi+y6 zZ+vbayeBg{_%~fg2RE#$4GygO>fqq2@xEKxcL%qu+VbJMk$y3A+_CD`hi_&7 z%MTA^zl!JT+kTYYa@(!!Z{hO?w=Jx?^Y(>Rhw+)b9c^xJtlDzhJ((@HElZoPqwGK2 zelWXX)2-~M@wtl6zsBc(+jJ0nb1VC?kG#6-pMT`FReyocfB49)?7|(lve)qW@9=pC zpW8Rz!rz{|l|6yav-tc5KL2d<`tJXE^HBF^vF=BQc4eO%TF%}a+SdK+L$|X31fTEW z^NV-h%Ki~P|LdLGy8jZ-zq#{Zc3;mxHn(MA)o1W|5ue}Max3eGF~{)1?9UAUa`&f) zKi3VsXFfB$b=9BrynyuGtGm-GCuo0+5nDr zWzT$cIa~VZA9puD`a*ZXR`#zx_BY+TwgUF8 zFJ`W6-Ick8=YO+xSN3aLf0X&dt$*D8ChBhcnOoU2_&obF2Un98#<%?lw6(44#ckWV z{ug}yk8QWI_ilf6)f4!9X8TN6ZTo|n8{2nfU*5i){cSwo!1K>h{=Xyr|02D1$K9*` zeEZ$2*6zTV_&mR3H1kjK{ML@oum0hVS5|W`{^QTyley9RBYa+4{Y&@U%KlgQ{LVe! zTb=#*e_Xxws7;xt%)?!%AMg1=?*G}dt$Y36TiF48Ht+or=G>I~-rjpMYxZ^b zOzd0Ep4qo4_gDCQZ2zX*nf;BfEw^pTJ%{vvv44N|i~Db7|9t;_JwM#PVO8(FgZSLa z-hx`lh-p#w8ByJtpsy9XXCo-b8v z#meKw%Gv5eXRVkG_Ux4w<5!l-mD*!PeoG5q&L;bHjO9i4)XYP(Gc$)v)us8u)kE`z zYSovb@#y_=YoFsN2jX(glokr-ix2HbC&0Kccce5Qa@C}~JU0*EE&CY2nA08)-8VCH zxl&kSZ(Zx2&TExo1)s^O;^N%l;(%DL(a+Tw=osPM; z^O!7F*-C^1@P~Qk+!0 z$!2OrYrhpr_izvnw?|S(i#2YH5{1^a%&+a*M&Fx^BdJal)?qQAyAGwJ`#P2G(Ncga z38miY;sVIBRwS`%vF=UB5jqy`*>8|b;!t_EG4b&wUAqVe>AjaTHE?kG}gAoYI**Wphfa+%@1(I zk=fZ=xx)GPNS<^c3BL+0Y$1T8B?bVwOf1fq7K_5pQ?*L5urOA=x;V=*QGcpZo-J0Z zo;oK)0%*<{Z0JD9snn z&KDoPS}Rt^YQF3Hvbs(!Ob4#V6v~LBqn(8&irE0-jtz_im}y0)Hc|9NjUo8ZxO!Q{ znIA2xmXS=X`Rd&>dywiN3Zz(Jh?09p-L&rs(Sp~K&81Bx@mS0~ z9R?o7#%gm(B;3-WskXcyJlw+Z$gjX1sOiidsPI!E_0TfM{8ahuGo$Xv^5QHW6NG!Y zGFwFY#PYmw2#d!Tmlui^@(XjqpXnmehsC*6;i*b-t~3kLiaw7n7nk{jaGYJPREmqW zhYRPCf4nfiT%0acFCslQOZ|$(#G&&1e9>}`6`^hy=dY#;C#K66i;GzPr4ke|J5y$G zpvhwGLV51t< zO(`axV_V1)@aUe7?9F0N)NGIl8y2?+V9>g6EMsX?COc3KjVCrXFnvcz8KgQ%85Vmf zgJi_wu`f331emu)sh*;$X3Kcj|$4mVxHM7%eAFcL3Mdy0VZwK zVZJ;i&rb91xTVA#E@@t3EYkyD$bEr zeVMrtK;uIYzkHBTJ8}sPpr8klG!}?BK-;95T+2GBM9#L%K}9m}@zQ5`2@)8rbF@-k zUMek~N7_qG6do^^FD@^k=#k=85mu)Pr3zagE{QC~z9JVSaH=>FS*cP8D)a|3367Tl zOac#(Lwm8z4xS>4+qsL4Jts*&FLR(QIf~D*jma6`9z4zl?bArHmosRQMk|F4yJ`L2 zy>2=g4vFA1u}%BXzAcOgq|yU(zoeLJ+0S}Ehosw=?T3QM2u(G^0S-i^k|t*y_JRT( zmY0MkJ4g{!E*3Ax5<;4vuYgJBC|Ih-!YJpcq-i9pv*jgAd#3R<7ofd-a!x&UYEvum zw)WHlG^8(E2zIxQE$(^XYd^7Y9n+R5swu90QhaAE&YfHq9z-uP9v?s!9=4wyjYwaQ zt!D@D4j<=cX2urFi&q!Q%T;;<=fUHclv{!G6{!jKjh< zZR}eDk)dhGz&09RDAmRmp(Ja|mBr@{uo>h$#kVmKN}wmA+?=Q4t7@Y`y6o2O_9N!9La-nwf4V zKi?7*7n*8@033))C2f?YmKnMva*dmee3gPzqZeABbmt|z5gi(utVXh=4vEneN@@}Z zK*CU>DPRm`q=5apqs;jo^^L(WM@mUR47-kcL$d;*W$djf(gYf%qyl+=_cNVJ_o7rJ0xLcJFsamX zU&oCZWwGL$Nlzi2+wSoV!1$q;W^5T?2(iEce$-}fGIHmEeKn{9sZP}48|-7fhgQ-A zcYjnU>2{Lw-9a*;zh)vH-9uJK{b$N&ed-)yj7lmfHpWKMyhCbP-!u?Km;tE};RQli z11Cs{X}XLjC`MzYzE<}g?YQrJ#M*%SzV8{9`mJSBxAy+dTZ`iI{M?kfgamYy4gF^O zBxYu*jVWmvbPpS}Gct=6>(W360}Y_qXdp32gpEiE_q+4O+RW+cBfj(Qna+dzYR~~v zo#?Ud9Mk|}%QmCLcgNO;rkddZ2cl9*n@Nqv5CgK+NZRfvCWme)@r&A7 z2{@H_bf5)i3swAa2?n!#f!NPJ?ZG89KHvy+-LFiHLfaK4b*B$>+>}uk`+Y^SQ&f@r zj4B&Z(z?%`hujl}Wbz^T&};16+*f1gkm|H^zQKO!Z=1xzwoO9eXMjGF!N^9A4sxv) zUqC-95fQ+F9x#uz$B0h>0Z7^Uk%XvPhF|5a;>e7qH($Y-1%jC)IGk~Z5W1)pnM&B% z)yjx9PCo389um@?lAtTq%*0_Sz~D$#LG_v<{c}aW&?i|6^8iMv_M)kXWWqorhmKFk za5#INEm!7FKXzQkjItcUPEdSplx6Li;@K*~kk&WKhItN8pRA4?I())SUB$Wa!U#~X zP*{X0Uo4JHoU~G;LKm7fRaPEB1bDG_WcpN8`S_73f&0YNkuw6ZBxX(>nmI8xIX*Kv z_Jo6lwOpK;i8`75C|0T?4X+*y1W-lEG)^vkUNKuYr9g&G2| z#kr(b5H7ii#gaT1g5SZ|OVZK+vOu2$lP;APt1{ahXT#<)%9J!%@PR_50c4Z}mH#P^$fjMR-5Xz65oha5Wmn#?P&>bz+jxC=hv$*-v*;(6<5K*s=94#*5 zG;vnf4_6rh?ggB9S8FJvq!K;jNO#m);vM_4Q* zM6^j3ngt;)m8(7@by_}ExMVGgRlo%U@)vx;3M$YYDObkfvul4g0!*4Yy3!FN_Ja#k z0g$wfvy2E%X=|%|)=vnXvCWK3`a>`t&JWQ1NYi5|IEN zLi>8~ck#4F<`m8-@0E)#2IVUIbwc*De43j}7=7gA6v8bPEQeKEEr^``hOpjH@VHA~cxDtlo?4Z`DBw0YZm;xS`zM_Vn zZDCG4ba7_pVZdPv%cH$6*d6V4v<})!sa4xcjf~rCRA{u9o78BpQSs1T9!ZGK zm0k>VsI)+P%zo|7+F7f;O(%A0Z#48&*sC~bU#Y$@hvUq)afPSbN~h$2v%Q$?)>dky zCA`^Io9tP8$7rkCD^KCD5=VD1EMb{|SK3>^ie%7aqB>cE*U_nksH=`O5`$=i72JHG zeID7$)0I+t&B=4;O0%%i+H3U0sBHpxLiCT?+nXu8Vgu63ij_Xn*m2~;#p-OOv_uuy zJ}p$;Y?1rdUNv1>fVwO!EVWmSE#t09rM(;*Qf)64^mQmdu?!QYauO#3-c})hb!dmJ zqKI0CZ`nS$4dO+1hZdyD4#m?2L>=4q2PXQ(4#hkGA|qq>+WUBCrbEr4@-mb~+a#xJ zCEl@-fL~js*BpMUc>!o*(J)kbyi~m0J|0Qq*fJc?BT!e1bMsf*n#i>vG5f_kK}s(= zdwF$$B5hes;K;uPhWo2c@t)yc}43%u&@l zPJH%IWZ=owC|_1D!QL9EqVHr>|F8e@_9)a7{UTFsHhjs(zuMWoKmDlsEx86VpB4n z*JEngt4=cy!K%2A!2=hyCwuD+VrvF&r$%&-6>z>5V3m;t%)|>8OIuvwh$3i=10R(U zv52&m2ohBDP}$9#OJy&ZKh2N^&q*MlwA;+Aw4W|q1d%LW!i~51CLXT5h3j^_Y^y{LdFCF_n~Db6m}v<(n><*FnPmzUWK?qUt1j(YGIVmBxOfiOo4(U&|tw&(B@ zqr15tHzwZ`^34e(!M5Xzvz4n$a428~6|^ZJZ7n;!#LK%n#`u+5rNAJIWZ?@(IfOo? z6m%rdzc21}3vgVo}DE1m~l5B_)!ul3G?#!Hu^jMEZ_N!OJ4C#*>gDLKg;fxz`M$kixG13G(p1Qe?eixnyO zfmo+2lZ*j_OwwZr3@n_T4;5((RPfMJgqa?`dSv+*e!+JlDi`_1^)OQ6QR|a*CuoI8 z_3=WrCW1#wFjBg}hgdu_YY*t_KwL>+)+;*(<1~6DsYCO)<$&d%X9QbI$_r=NM#*M; z5kymTyf1=x4|WzcJ0amMF_yg#=cb^-XgpgUwoS-zohQl|=V2(|#VKA&5>UBGf-p(2 zRdBea0k>4@VCmuFFq@w|Cl6pko;8+{K&CDfDp=WtBEBba6B(iisY8V&To}>WaA}#Y z^u+naa;10(=PWYCAxtqZInZhxoi*Lz4$UKGN$tYer8~WNs!+ibVJS_R^oUNr{TYCp zT%Jcj6+38mk!Ty>6)L;qApi*41Phv)piQjFF&o?0q_c;qZ*iOX;Y#_kXaa4uNJB=G z4?HwFGgIYG02F!X^$UkMXXP?Oj{<@BwM{ZrBsh#tb7M8QAZM4+Oy{Eh2xWF-Je0IS z6snAEM1qB4zBB{!Tf!SO!W^Ndjl@FLG>_qHM=IDkhP*?C&3~*khj>^hhr%f?(g6uY z1`kQc8=dWy6N~30T85s5`4wA13ZE;TUq&P^HN#`Yb5n@l)vi+H;++#i?&+Y{XuY!L6YGmLUO8*6?Cm;z$#!@O<$5~r1k!- zm+-huj}rh5KY>T&RV2%bVFg0b8^N`=nbE43k_sz$ZnC_M=lKB}q^(bdfqDK8YMiT@)-3U{m=E<;!ti z)&L1pld<4Z=t+u#p1|02? zxE;CUKr^#tNywN9J61=$RpKT~vz2nSe6BXq zp%T$?Yo}8}qFX^)0giYk<(z17rchaAa=I)U0!!DJioCmlD5=j`rCqY1CG4w+(bkBJ zY(rb&)biP?1Z>B{lKS-gKXBGwoc)6xidguWJ`;g}%VR1C-HI9RdVlEC{8-f596j^NHh96$$2*7LZe zInrZAo^o8$%IR_{%dvfXHkCdl4Z7&iB9dG?~KA`}Mw zCFz+GU&!>NEG3lrAj5~}*_6bqsZ+(`MVN&6nl4Y5XBl1+5P8R364X0#rX?k!8YBh! zJOsN0M$-vG>T8!PoDh@v`|Ym2x<{Kmnw~p zNr9y*0t$d~9)ps57xCmCA1_`)3RuzvtnNu1+b=9HXub$Pb>5lQLLkYVI$V~Ap4vMs zi_SKSIGhlIU@N4&Lh-j(03wZID@;2ABXbz%krKXWfFQcEFyG=Tj8Nh74Z|ui#F5kj z9=X7_1OZ}L0N~RAkJENY#AZ-;QFa005;!Mp%z?n2vlnc%BqK-z`LW_CuvJ(rokM)r zvI>(8v7ByXutOK%eI@L}^5R9_-7H?DA?!;Ityyx?Xk`Xu<2OJRj-2}x&JWR&N@IZl*|#gpfZcuLg;hZ}wFJPb{S zkKDs$1RDyAuDk?Vs#d2#TaK%b&hPygv~OCxtg<5ujx>=6ss^RN41UIM7jyh8(a=oYQn>1q5}dBf7i^ zAcI?)g;Un--5!W1k3B=n< zQGxl%q=cyWq#Qa$1#`u75Yhq(2?K$7%^=9j4MDu8Fn(ngb^&26%+1Z5UIberBs&+1 zaj8ZTue=yn2{)8$M{w#Abzt~AbO*nN49PD#9uv}t^NB$q7yAGcJ4{q@r>Mje$*FQc zDeo=G#vhFkdg0VUU zSYSgS17}uXOpf(rNg*d54Nm)@V7!cjC!~hsblIK7Jue9%k=~6~z<|W7r<}tT*qBnC zVA0HCDGg2IF%Fjsh&Ay}T~!7HJuwI&O&}5C?BV5wC3v+`1?7PQLP>g4OH4@XCURHy z0u)Yw8Q3{oD?4xNGTG4u0X}f^r#gag&yt&(FBX>~DG*7U5dflSvZGKzC$$8u-39dR z;NV06;DSD5Ie#b0|*p(ZA5Da5>cihY>r^&+&Ev7m&T9b=!#)$L#o!u5;O_s)r5xb z^gJ4L#Au4okO9RZz?d`V$d{a9o21D4zj389$JQ z@|Y~cvPIV4gihXa8mm;C1zu8xzM+o>#Lc3jB?Q@kod}M|sIV}ZcrH;BIxS1~S zMcN7Pfy7NIZE5URX;vbmX}c|?IkNW61iV(TK=|bGT9zFC|(v#Zl)*1~jiybR2~U0p0UPk^-J& z%zeY}6!~g%uJk?$;`iQ@S*d}^lBB+BBkA!SBnJ@S=T+Z8FzS;AJK9KKiV?}G$d37W zu}XYTZ^I}vs#1uHNY_>O$RboaMk<{v>1`ChT9`Yn7xZKxyLM-yPHa@&z!j>N@fv~M zDAqX_{EG;h3)`FGG#-P5TQOC7SK5AN$u9C}o{?=)xUwE&WVy5J39E3$t4Qqt)|!(5|ki?6e}1{-lCF87^Hw`#C$xIubbNMkL>J}8_;#GdqoZ=Q)1m^U|ER#r8t-UC6>Z4!XvAF@^+Tez z{wy@|SfWbKP{jLr3647+r+PR>uv`m8LLzrMvG$XFfR-J?l<8icZUaHI(NOFYR9*c3 zm_{J0&aiIz8Q9~nD~j`0khX8FR5yQW3AeSa$U>boU>S7+v&_}-txk6ew8Tk{OSxnE z66=V>uZ)Pn0saP>xY5eOJV@}@&V`ZVeDeD6$g$!)Vxm>M$ZFXZrN9+}Ixsn``>MB- z5nNK8tn-QBKZau}m2_irD;mcp{;BUc$H6?$3Qb=@=BR%bLxnyT`bmL zx2w|8lFK$)NlEQ(C!H?Icz-R)vwf1`65-=;cvTj7Kkpd1=#Edd22*hb|OnFAAObg3{vb{PJAUuLK6LFDHmTS=FJ{E1E8glYmnT zIw_~-c_mJ?Hg0ihLIS9~dnKl0%g+PTrLEu=6~!}7Q+W1^Eu+Zi<0#Y+{J_QYkQ=no z;AB>L1|?~tH!*?r>{l<;fGvDh^pa1P<#ZULod*+`4$O%s;tL3mApt~1iZrtABaa5* zQyPI&Xv$4eiX!B^R_t?-@2Dnh$BL!%7i!>JdEk6y0tfMUnWx4RK_ErbycBd1Wg-Lc z#sVc|g?A@m6#<|U4jbgHW*O4QFbJ&p^d~D;f-9KSTF6cV3{ZIzTJc$LO#2+k+$S-h zPhoLB#Wm36fbf&aVFKU>cYFZ@$(I={xm19kjuxkiP||qn8}WKw0xq$Ll z&mB6-@c0OiBXO<5Iut_k@@IS@y(_C{9KxsN=7vv;*Dk+hEB7?q%&b0PD@@~jx5~>K zxF>R{R4Ff#;S)kRT%HvP;8)1s&y}PNh zp8Up z2OGYY7K8!~E(k{6mu!GiAz8sh+5B-&O%VUni-ALr3Lsbkyda z*vVI6ODRQsPK7$}kO2FaiV(5b8`X}CvtftiXBZ@{vE5)b3UQ!KWQSSN%q$59b;|+@ zgdd~vN0*i{5UMWe{^N~s0Up&nV?a=bfshU_irFCV(@HmG_C)!zbP)P4o+zKe8|Wy4 zyMS*H;P|<7kjOawqdSWW7IJZ)Pk&2tWa=txl`9m&#VZa!WFq7&uJu&-0YM;N6j)rO z76^Vd`&{XYlV5m!_^Mr&9ZQ@k(`Eb)w~bb_#6$D?_r*# zkS*w~rC!z$z}id47O#3`h;YJHxu=Pt_*)!gcybzUvn=2)9#$NFx)xpD`)_s#lsv#Z z4Ob60nsJbQ6=yi&gdLH1CjJgDjwCNh;3Qps)Nb`yLlp4HJ~4yrj6^(dm_^>m4E10b zjpdB<=Z2!!<}^<%_^TDVUytgKk&8z5KTOW_#3F8BLoC$*x_^go96x(})h#h3#6MY9 zfAt#v^xQE1v>h%1;t$r%;}70pVfbFapSl}%58`tTe_rl+{Q0?I{E4>`THsBOmikd& zzl=Y1H=p#{<4Z4~HSGSRe6KG@$OM1-F3hqoDKCSuEBKRpcxM98EJbbhqs=fr&pUTo zAwQggycOaj9%3suNKdyx>PJ1VqfX-w_*oxY{RFUNWj4-~U&93c=wB_VwUt|~_2SC? zJoRtrL`MKf8OXLKHdzdSO%?-SQ=9^FYd{rljkciE(A?f44XmM^sKR9uY+&n15z-rX zDA=tO5QANlK{(0042j$HZ9|47#jOs@VhRZ|Y>`BUj@rH$LtOiETSv2np1~|<@qYn- zxUm4#U2KDn3nsmf2^C(zDlUNr$7PQz$l)b`1jc*Ao~|+ea1YJ{;`3;=3xDXbb+(^|H{X}tV=>sWFB~H7`7y=Ov3v#{;uRT zOj4BP%;WEFE~7Qsi_AtQErQ`|$SouFJpLBuF#gJ}_90;BrRDQ(2g+GW5y3g|qgY6N z0!uBS_XVV{V7wwm=ImuGE*{k_6YNg^596)d^lSlg`Nnq!^=1S24kD*_2F&-s<(FS_IJl)}0M zHbxx!bq({6p>~ZpcN^DAQC9=*&H}0eR&x=5q?Lam)D4iQSwh?@LAd&_9YtmlpNd~H z>5%-=-dN>z87&v2EfGZdMJ6A{pGhsC<|0bU*tSKX_RF$$tUZrbqlLX>f?79zQl6R{ z##kg4YNK;ldj(G=*Sxc zv|}0O$n!AS z?r6ma#Tj=8{I{zex^}GZ1O#fuW$KO2Ko5hZ+dGD33`1HGl@u&w3j&PST=937kE3)u z(jyQ8BS;$qa6@;YRteU&Xa`7l!MVZKacD$3TzBt0x@|ShRkl#FvCu zo!8BIxE=1c8L$%J;b`K+3WwUYqB1G`Gn-IF;vQQCx>2)VHN+N%s)tU0~F;Y8+n? znmaA+gNEp2<($H(W$0^)yH*Ucr<%&Z-yL&+lA3xa_Kf>T?pYLRpQ(TKu&<Vjf#PShqk+I*Lz8J5b;{J^T@vY=VdAFa75_t zf;q5;49^ifaFelnx3ws)p6e`%&2^mM!e}x&OkdNJ(im^)n=P%8Rt%q%IpT8LefkMzhuQwSewree0fUW` z>2DpGehT4^+t@6LwAezeS3{e$hT92dqIr^el%5#r&33r6xngWdJf}x8O_b=LM<#*K zVATiRVzd@(7ve)Dd`As9U@^C7jWfrv4~)Ypx1>4BT=!1i)i&WS8_^h<`$pH5d5)MF z{h&N!Be^Y>+6nQbS~$Rq5wEpFZ0!^#+hgv&;e+rnzCuB$!G~6vK+kY>)eB5H^lW-m zl^)jDZ3&R?#Q5w<{2TXV>c7@m`M;NYWDFCQK$XxcOcsC;hS10iBM9hGCsO25FcS?_ z+(sJ4s)20WZAU%f4Ld?eCgxJydSzBB@i~{-L=mD(3Q$d;2Ll{r0uz`kL1Qzz4IG`Q zR3F0aok-=0crxaD>1fw=8RlAh3pdnB*(EdYn#9N|%(+3sLJBm;bt~tv`q<@dHKSB$ z!>sK>NM@k5HSC6=PCK0RI(Y<`RI~RQg8F5N)zOS3KKUfGnmNh+QwJdqelU1tu8(`< zryY;<5r?Ltu^vqC3B$ATyrhA6&vtsFxCFYo>N3C7c?Z+No)%hqRIZ$Yhc)T(t%j69 zamQM=q6+33xTC?~ar-;(>vmw2aoQDjcY_LVrCaRYrZgSfzMX2IO z+jg3=gY?K&#JY)f8ZmULRKm2vtm-(-Zi+qAbq?}GWf0{HtmH@2*pBPymT?{$e$Pen ztkwC5+rj@gvw$+~zqmCc z0yWIU4W}_e7{yH;5h^D4lZTZe)x0vQ4yxK@^)+W4{yah|JcJwvEGp-5_&2ZPkqwixlvWsHrgbWUK82Cad{FFFlj~; zODVZkdT^I%z!EOyVpsjNdqPfquE8u)(}aDtBeoassGh(~O0OWd$2+W#5@@G;2;XKL z>Rwvzd6$`7Y0hizall5WUTIIyo!)1(!B1nZr}2qb;06PC7%m{a>R;_d{-Hr9TR14D z^FWD4VyYsZO;D+kL8GKquq*keH=I#Ai;bC3qA=Z#u^GMQs8<=&io|eQwb1hJw2r`l zX0jL(Ak@dmTpE2!k7iy8i9#LsSQxALq7mV4qbETzLA5}VX0GBg_f7e7!XC=QD5~B_ zxUmXycZcx^Sul}k_bQ$?^JMGXiXF~IjHzLwvuO_sLav0y31=CGKV}$8!LWf2 z1>@;EA+llaotg4^3=6?4rd>kGmOsA z&0(x8jy(}adZH10eeP~pKPh4oc#0!2X4JW-JHc(W9K)hn>DT}xg*13#h`38>7+lv` z7%OLG3|ui3=v|fODto5e8r7y-b4*@DP%||>Bk4(_^;~T$JrdVREPE|+$@xJ$mmf>g z+|y3$*OrtDoI$mrjzIp>vnYwy@Vd{)LYr5Kb6OSS_UM3HVNf#q`bwwEHCLPn1$xJbuQ&0s0$Xq+ zXTdfQGaJ&dyIo`0AXP{-OxhVXy*9Vjh#hLJ{>qf>)~xKEE+teO_ofFRIO+&nfeR7WD*CH z$(*(rTZ--C2~1$oAgU9#BWn|QJzlhy++pICI}=BzLNE0)ZSOQj;s!g_yB&vCvoa zA~c*_vs~p}6*`P(w_z^Vg_Bv44xzm}a~umu%M;~$G8FYUNjnXRu_^0pq_j;665qtF zqpN18*CVpkCD`Lyqf%M}GD+QS#)N7~B;|Ld%Xt<_;^7S{5)ek+ly0#v)NF07p(ClQ zTL`W0Zf(_Jj+QnZ#~GsTp!n!mdxzC_7#)6!-kBlRNyZGznj?;16+}t0T38aXo=w`_ z!E&`EXsyyuN&4Ki0NG8fx4Qqmst^!`3gD0i1`9o<(EN+Qgbbtgih7m3a9zB)pR3DNOj=_jD7I%8cse7pi zG82!>E@*4ztaJ~xC4%ZvK}IRfJME=k2#aN;*1l*n|DTa-Ce zpOh~)Ryr_7c^3}$V;D3jXb`zA@p??e!geWti)JiDQFPajVu zm{K$Tu#8ZG2GQJotw9}n3+%}mEm+6Z(&h>wQ@4Ymx8Tv*>^sqhBcB(Vp>L77hb=Ws zY5^~v^f7!(9js%MLoeZauC}%73K<0Wczui^Hl@c;yG-}funk-n%$VC)+j=h)Epm!C zt$z$Xxv%6F;tBQrt)pfJS<>1vvyHS(f0+2E)tsge$_4)K>YqP}<)+USI+@6ZB!)pW z>egN0Az&VDRK=u_|9sd(rUyS}W=O{~qLp_k&nz%}_{dmbV*8j}&VZ3PHe;PSaGB zUcbxn#yy13mY>Cq_lL0!q)7Fgc-Nh!!|oZdGCl*$O<;U(nAW*bGUZ|9;}gl1HiRLV z%mCenY4lJf!ZNpuqj2Q+-&7gn9zxPS>!H- zQ6s3GkgaOHztGC;A&zKz&xnTTZavs>g=wIXT2i$HZ8MYXY9-sDURauHF+zktzBp9Pv5!&mpb-5k*#uH>?_Z_K^7FjlN0*F^VqIdl5 zsEd%D2Qd@V#HWhu*v%h?$j0fmd7P7`q^eB4H^GH6q($@-*<5rOal~cz zKZrMb9z<#Ussp*u=A1%%nqWi{U)19N?kUPB2^_wG7@6T2s%;HO}+ zaO~6I?PEw!AV7EoC9eL}PN-F%F@Q{zFcQkiY}a^Clj2MDx(YVtf{;ifz7wL~8!^g% z_J#+ng!{(9kD4bcY1dE;>Y-*_!*U+Dak+<_J30+GHC#knjS-P~)P~Wn`caN~^ik9t zaeTLq+@UB1XPo??<1o_7<4O{Vj>Y%-C@qM%5UhlC z+3t`mG&Hgue9EAQzHBgzHSSN~+TpRlQCi2a5kOR1C$P42n3=R-GqiejjIgWcN!7@_cso~O!15Gk!zZM3us2i3roB#bI$d8zbj?p?YI&cv(b+8~G`|ra>)q8oNn-LBF82ufAoh@|U#`MNg)8pw)TjfSXm)lw) z?z}R0H+5$?@v<$9FCw|y9s?d|b8}rsum<7==zC-w+|J8l`o>1+8*~!a6msvOp%BBN zC7;CM9pP+zDjgpqGgN^Ylg7dPCC2QW)+VD+lMs4v})P)ll2iZZ=w;UZ~yQSAt(Tccy7#Mbiej%#A?x}zE^G*|-uOlYcp)BE=`0z0Wj+eZPZ333amsuRX+1!`Ms)QKEJzmTyZ zu81_pc8Cq8r3}f~PJcg`*%R8EsiRRCnsu!@!2GzhEDdu)8+_8Fx*>@OphMBxX9caW z!}=Q;rnN#iCv@ZQZ{UP&HfXkCD^GX~J@2;Re%@E6fl(?r(hMUOq-<+${(!1K?l*55 zlXWN90v$+y*&m*Dy_5(4R5axmB){{@!Q2N!_w^y!4#ll#+$G4Sul7Bc=IZ z9U4Sj-)`8);X?^b=vEq?)-Y#K50<+@i!tm$$2mGeOai4w=5JeYL|WUlOv7c-Se{1^ zuZ=i;yj3O>4;pLg+?%rI@zgB9!GlL+&FUrT*-6;-&WsY~ar<=wp*! zDcf|DL9U*p9B07Pup9*hJzy>yR-uW zZ-q5qLx4whB-YUdzRGPw%k4NBviLi97gqo1%CM8dOh(Yh^!^rpmBZ+A5gg0tBypj8 zX1vYa?c6^zWSE;lA9}enkTE#qIe}u4Kmi2VDUX%}3650wI_a9;#r{wXfW`zG? z@2r_|KZpNkaCflNd8^yex>MYV887T-5}Br+zEVDAH|9fl+KB<)C* zmqt}q7dS&>N^0Dzw&&;}T0*O&6@FYedat4F;${2LBqP2*kOnIRCxR)#HhO)C;wAiS z74M$61IWuM8lGzh_4QUNRXzxqP!nJl^u%}*qw6}QMIVSe*7BWl^FX)|mIq62*!?_$ zqNe~JV!!1GUQohazL;+s6(0P@e=rK@;xxqi( z+7U$*fntks34Wu-2#`5r5w$K9RQDtfb!c3=oYx+kB+xb)6i1zga5>2=Wd zqz9unYHh8i-IO~Csk<0#UGn~60?)i($d~!-VRV0=F;%t5rj;40k!(0NLr=_ z4u1+MW)d!&2(lm{&#*f|zy(vCGC-g#MS)1tU|+6a7s_dZ;YYLZ6C%p8G?qz5ZVSna z2Z7WCWYhB?J!=I6`dNi)Sy~ZKTo;8V#VT=l5h+G&xOo;xK7kbRLx+eiEYtkZ&ZJ94 z4%Rn0+XEJS17F7_3DV_;LvY_u2L)G72;FEqXUCXIOA_MB4H(f8m#R|Skg-=gv``g8 z=~3v2gkUhu%+`=hs{X5{zH|;J7d_!o-RlyGk5WicZG*Aj)ksI~h<;UMdU)+rur9XvkajSvA$Ae5yV5HpOy{vjD+M6jPCbUu&_$~=(`sF34hG|?RS~`KOlMmA zevs=A!FFG91^(T9^)Ev3!9ChKU%6bG2=*KzZtWsQXXDMxHIrDa zvPpl>Ony6{TaKdohp{_Hz$tXYWCu5}L=QY9zy1NtssiF6oS*V&itlaN8bXjs4Hd3O zjcR_o^+&N5YDgXHM4a2w?igC+LRzn7n5(2ckN(5>Ntp*N(r@qwj)}QI_sVW}*$cI9 zZ)@*3e6JK~!Of)4qf?Mi3@qA=t-;H*#Yhnwzv`%_lbS_3f?H!ZligbGgTA2aHhdZ$ z+ew7nPspzXFnTxK zVa69=GJ9J`B4xc#pSy|4__zDGHnn&rK`oSe?h(=&rH;NrXZSN})u0gZ!+V9n{xO3$ zn9K-rY@Nh>;avPNjB2k&jbMN9hec>-ta$(<@i!eD_~HRrOqzR+B{;m5XZ`bhQ<=^{ zxWP|01ks-qz1-Z6(SyLHYaq{-z!` zmc&FslWwQN+>m1Cr|VatKM&|QJ0deUIkpog+Y$=DPNzLV3~^<4{nJBqN2k*v&W1O` zPcz;}IMpK{wC%0$Lg^#eBASQ>iK$J!i%XM~u`WL&PC(6%xAt4n*WQ5B_iT-Ex4Ne~ z%;`tkMFLVGp=Wcd=GtNO?blM&k(BSkNyfl7Jtxvoi>;6%FZSLu2h!(hohU{7q}zem z$L;d!w1?7Gn{L?(g*g!ZGV9iK>y_qWU^%SBW@;63@u6<(`?Q}xoS#ZZ#fGsren90= z=dpult7?lPGu(S9fJRk$()e@t87^AKQd{j1_9eDildHUwQ{YD4uvH2&SA@|W(r8$* z@>1)1Rcsd_ROMu+%-Q4cJNLS^j6&G@r9I%zGmZ&&X!I-H*9|d%sjtr{WrlHqZFxV6 zW2PYrZ91(H{o3x)C=O$jk-x}52PBN;1{rQ8U-E~VMgTK0&R>18-{q}>2YFT+>S!>~ z4WuGew>qh-|9+Y*iAYYVv0D1skDxRDaE@$|k|UAR*+!;F)1aLZE}tAzMFsz440|sC znWiE2t#uNOUKmxoNA1HaasirxOY5tmMcpI+a4@^^GeD&hz8wv#?P)9cAoy@Ir+_p5 zwG1*ngQ%)tS}kxRZad5*E=*yuKVO1NADSu@nfEw`4P&(j`E3mrP}RJOTC3wL4qQ2} z53$`!zQZ-FOOHu254KvGHr5BiRof5XcUGU~7+wQZ6zYL7+ga@zaF=F3i0!*#DhUuq z_`=U{hqn&gE`1m2am+^rsEW&kI2BY-=3?u2%Zni!CV;bG_3}5(;&P};-{9TH0ZtOrFjUb@|JM0$}kX;4tfr`5KN>DEv= z&@SU^klT6Ld+Re|ohnz8BKZTm^nQZSli)+^&joanMZ3_M@+bbgIr>Wqykn=oMNi`~ z{=tAWZv4^xr7*F%uF0FT#y}L~{LTDep||RLb-ZbJ z5N3Dqz1k|axjOCzcbeE$mPFt8yy-#~BCVm_(ZH7Ky1sX)#q6ucF<-b%1S{*V*omDG zx}k82b`I~^$0A1IZpA==%&3+Y4sp)E9l<^naRvqLF8TzjLc{o(t({vCF2#`iY%MHk zwHDLkw2H2kMoWvl%p(jg=n+tugGRtHKu_UhKh#LNd~ZV|(*-AclR~FdY?cu1=gbBR z=gnXoh}PT4B^~}kLKl7qYLGiUBO;N3Oztr#7bkMuKmKtMRUv90u_ar$4f>}$cx6P| znRAOmmY?(4GrO798)vdU2mq)e=SeUdB+ZE_`9tJAWCyH;9}f*>%u2x=65!-i31C$ z>R)2*Kbo-J6IS;HJ{9(Uk4VBssX zwwJ2;(zoBOES9!zwA~IlF1xT|7=El1?X1+=v_~8O&1@mGu_>FjH{5y32F4a_HHJ^_ zL%J>9B)*NS$5Nz=z?(c&36`44Mjcb=tm`S~3h}>^;KvHS0`yj>6py-fBpF0b3Zxr zM~D94`gcG7kAL>BKFPoEm{~QP$@FyNtACKmK1q%-+0A6W!0(~%)lAoaM>2gI2lG9v z2i!nrAUn`Cuxg;Y3pso;_+;_v!eAD`foLd+utXnUEj|g&I0u)8|z!OI)8n9{qU*`ur)6K#(Fu>^$BcxqsJ1f z(Hy8}ev%cWh5^TMpUpqJ#%1~Fzka$$Q~Bi{thm2^6ZA!xdf@g;7G1P>)iAbw&0xO% zLUwb${%SXd-+cSVjTrz{e?2K))tkv*$!!GvJ(+)!ndh+7eEr+<{j2i*d-DBtRzAV6 zpX1kIDg0OR{iiIbN%9Nw{VVeQSr%X5*F3+L`1OF){A(tk;@2^Lo#NLVzxMI#8GcPm z!#b14`E|eKzr}58d>mfwb{tZcg zS<+vY^tUAatz53Fzy3xq#HGU%@}>v0pG6Womv8)b?`l}w`Q|32Xe6?mK+C|`3z<8? zO#T64zteH;am${MY|Nr%^U=1JcV<@iX3?}~9XKEv26OicWZx9XzPZkEw=uwIwr?}Y zqDPVVXHxrTKbOnp8{g*mJHU|5odEf*d~;AZEXrr2G+IQt)x%ua;9BW{1Zp=cz@yna z15{Fdr@J%LLy+03zip27y)g(7r$9?I6J-R!9B`;ggS$3r#vN#`S+yEpkULpCB-NG4 zH+~Fh5ATL}!*XkZ;9E}2`au$eD{x0mwfpL=6 zG`^c}{6H5YNsNGho{ioD*80}1hKT)2wqL6w97;i7U$5UP&BjQSk{EST;87=O$bSyp zBakCOA)KMKR~CQEc>OKkqW;EOP#y(1cGh+ELYh;m{jFf|Z$Fgl#$xjIU(YvgVlTg# z0kSB;I0I4o1D#<=S^O1IPmT4;@r?sSTQAs!WQzfM#1hyzkXffiIv`L7p6Tmb)r+rw zfdwN3R_MiOgy`=CqQ8^$-%I-Mfkv4O7XTUss4ZjwDqA#r*aF{7uaopTNpF<&MoE82 z(jSua$0RMnyLmfs)VzwFe@oObk^`cCo41RKYm70^fwxPi?cAX6gUTDfnS})JZ-^>y ziuA%a8_sV8`oE@4$Ts!2r02X;*18G*D^gMux_ep5FKp0GL?3z#bxR8ojmpPQz}CVEPIILZi-NDK0~x(r@KK(roX`_Z>bEU zm;<@Q(oSR;Tln4kV%R+nj>Qrjs$_mM{RJ<R*WZwmUzd_!@5xaJ zgG*QMhApI~P~$~NYjAB}AB4f?b-060^!Jh1b3O^`12IE z3bMxU;9ob|YWDi8`Rjkg^7=RM4*=vFKauY@yPP9c<(n_l99WRTl9!qF7xVR(uzZ{aB%Nfg>#yanzupJ?33EdPyb0^&+30#dzw6&n8}99VZgmf=gnv6k zRSYu=YXhm|ufHSp_0MrtJy7>|Ixdw4H5n_UICY^nI9hSom{gr6_`a9m7slSsQ06hhJCKxjHcff)92F+IXY^;)R8Kps~ns02B z2L1hw!5-M9Alimf^1GlGq&K2M=KNdv{yQ(K8}#+yZ}j9=VCnjjjDRcC4YlgQ6Qd#l5*P zigAYg`Wi#o4`F=?UkGh^ORX^^ME)jJV!rV*EqQ9VyC4Pd4GUkgJLG$-6as{gQUBdI z066+`-N3!jzlfjVY;SM>jR%zv-lTAcq0NO3xL#!*Zk+f`ed;Oo8UH}LZm)kfx2-3? z&(zsF`T9@r5BhVBOo1onVa1_|3Z@M#ww@&1+}^@gg}2_M0qT z??nQV@RZg$_$`ee6I(-@#TYx?H@F+tGr^@?A#~z`h*&*q?0kKCK{Xjbq(U;&72@1jbJ9&^o&|)aN(@|`SKms^7CPfhc#CYQ; z6s}lb{Y}5V`kTr6KyQtYM;rKYm^nk>%^Q!>%6k)flsfQ@UZ^MxL1B=C2yTq`WsNd3 z;x05YvUF;BY=Q^5F=>suv%#nip1B`P__W^)K7rgu z!9R=rjiMwW_rD@0Irr{qzlTrT9zLC{vGH_&V|QXi($?|=hO(lS6`_#V(-35nta_N) zJ9BG#@(%!{=T%W4fhC~HO{mQJSJo+CXsHL|c?0A50LJtEjeQU)=sE#KnMF8v|Mk1$y)OsueO%{#xW7-z&#d&b!$QeB8$Xh;+7ztYJAfg z-%J|cj2hoWV`H$J%7r&|)p`0mBGIrOWb%ixCB*pWxHq@U!u)FDMF}6SftCkC&<^ci zAf~s7H_e8?g>Uash6d_TQ?9==C_+f^qCSu`HdM*3C8&~Iget$M$IK+f{=OJ&cC!b) z-qSfr121|6)Zgi)0RY2_AfdSc*vs$)swgm%8fnmz%maCJ=f*6>Qqm$O3zX+%5Rk0b zsK_1k6C&UiSgu5W*K;F@AgEUUrZoj0v{Ss9^$9h+UNrTRRrk81yLJUy8;L(c* zHXjxz7DLXhzrB${91GSm6hLgjknZi>GAybcpa4SjLzS;l&_ukUR>d0HA^Nd(G@^~eME%)#ht?l;Luz!N`wF-B59fQ-}s!3v9yx}^^D>%-}sJT4pFfmD|z{+Zh{Oi z`oFnmy(nWef_c$gLz5VN!7|{g8!z@5v%S@)ssn)xgcE}t5eY+)sEKTjqV}!6zKz}- zz_g5$A^a_}xc*~Lz4ag44gH;45&1z*8SEh1#EDf8h@&Zq){+12NH?TF3 zAb6&jit+@d-fETsiC+)UI|k4@HYG9GcrMvJ}c zEbTe5^eKm46kBrhX73U;5=rpfrW}qg=0pI1iv>Modmts^ikJEuH@1-aFMuEwhcdxL zB%@L@f7S%dXC>JFF5Fc|PXU`hv{{tIZ_^xW{I#fx-`-6rG!ISigCXu`zVRyng#H6f zIYfF%?J0o>A%Mo>349+&&@HIHo%q(k5?FX>VBC<^Q%nM|#31;Ub-QI;adfh)dugYziO8KVZ6-pO6i=W^!50Jop1>)s zPl^NN0K88zAq@qEn(S}<#xx1N#!3VtoAA;=<{;iEFJmjON)__Jl%S4gxFmXQi&T<) zco;Sx43aQtAGG1yu#!@Rj5VgF8Z;X&bnI_@8IkG)I6ZY(-#3i-e!ol&5t1b-BdDAr zjxU=#(W~x6ui@^?y*O^ruOUqe3>nCj|3*t8&@RS&&l(2OHF+n;*&D z(UZTDRnk>4$29#j{Nt&Ym=mC=&2(8{Q-F2yjh*=>C=Ovq`2vclG}46sP4KZC)Oa6R zkqn%r*7Qx<6B3{>z`uDyKU1zwT4bx6O#1B{Ea{If(@_(Ng@Q1R3V6jfy-(I zx49Lpra^HGcn`(Qb8Q=`ex~65E!)4Ajr!AMXiB_4y*IZW13{*&A!f{oF?7olFu=+5 zMbEP(OdCZR9ykF?B01pgKeOQl(Me9$k>oHOTHcxK&DXyRn)+`0aWIwuS(q=t3Fui( zx_h^;w{OGnYB7-Qh6aOW_3w^Ju&hqyFK)+B+{G4>Dn!#TC>E0f`7b`CQ~|{qwdxnq z7MJvq3Bxc9OZP$4HBWDVq>F2?4gf4705s=C!Grncn280Zm$;wx8@h-NZDny$Us+dM zp&rMHI}uNmo>tAs&1yZg4uPg1N1#BwF`iMsub8Tymur`>pLq*?*{Y>fM`)&^yNY|i34maDTfFMOQ8MYp8 zGW>X+;;a5Ev0fJY6h!;2uNvl4Y?xnt@6_%^5 zc!J4OpkEizCCHV(!Slj={kJ`5OAG{Cf-yJZj3{6K0;~r}N5-xYfY0Cf4sa!c_QrS6 zrTH|VFXUjVH+CS7>g|rdyPbAN{a1VP&7xmnQB}Sfn$04lpvOYduuu$GK=Z%%&Z0Cm zMfUf1L#W^Zt!~Hk@ z8oedhvLkWEk^@^8<<3C?l zf2VH^{5EmX8SBPQ_?l)(s2mTyCxSK!U|`rdyOt`%!|bDH^`98*00}1`mJ^dt7bUYT z;6Mz5F5g@TOWc4Saef_#59=4f=n>Pa(}n8ZpW!t=xE(ejQ%PT>J(&H&xrA2r7GSh3 z>pU+s*D=`7%O+%0uo_G?;u>TEYQ&|R2}RiiiRNavdRwTs?iwns`W!w@Dia!5Yv*-QuOo#u>jC2G<`6=iiUZ7OXak^g-i`q?~akL&uBD7$Z?iN70#9fpPlj$R+~ z%O`yziPLcg?;v*MskZ0qQN(v6kl?G8On?Dx&4hFpRA{olc_FejUq{x~@H*H-*1!a0 z9(5A5mYDRyEE`pgg#x)NNsC{qVei;tWS@Z53ov|!x=%2yQ}W2h$y1Yo8Q<9rn|kA&M^ zM)SV>%~2ygL`UX5!wY5%bMqx-96P7L*;wOka*!;Q3FzKeeI`kWqatQCENe7S4r3y4 ztxzTU;~y5E=Q#^>pBzYnJrTS0={3nr5xu{e%Aqvs_BlCIes1*JkJ*HQ13J8JXk2zya#Cm_q`4X||PdVk5nw)TYv^8Hz(C|u_ zm;z`C$Nsc2;A!DZ6DjZtuOf>`5zYAj6?ZkTQC(O1J%f!gv4bZ-N(I`9OBz$+UkowS zb|4rRM>Y`bkkB^kJkPwb2j(v`&&DpGof(H!E2R>Vf|N>$lxh=g(3UNcvfWghs-SJ8 zAT`}YYNfJLw28KrT4~#+QY&qx&33=<+jZN!@v`v@Ur^x z4Z0f!Da~b1ncJYw8qR)w8W-m_-6f5N)&i1WRaGqM>}tZawYDk3xpXDpvT63@c|JH) znZa4>!lfqY3Gw?hATFle;N+n(Y&yUj2&aMmA!3ic;4~{TtUGAm=3H0NQ4n!lY~7F z<++WPY(jE5RlF7{rBSc~FIG`=M(xxY-K#Rax_Teojm0;)UXz-5=L9rTKC4? z5Mzi(3a9?Wt2_ue%M07l)b!VDHj)u!L>9mb$4R+k#9e^QhG`}j&^O>UlRVXqnD5J& z#iT_8G!2~^pn>@?98_op|Dj*pt2*{Lr zp%!_nPa%~CSs`r{gRH}*g2teNMqPnG=#4Z*KQaPuzZwKhJAoVIs);Y;Ad~WiS~jKiSEujb2g^fOpsRZE7SoB?(f4g1G7)p!(CH zEh7ag6Du+Rbu~kh;0SFSX{5%Yh47Z2(?h%zQ64AK8LSkZ!-SUO8Xi{=QiCnWP=^sV zUy%8IX&?J*X;6t;-1s3t=~*vs=M=Lvq7Gj)HZ+1g&8*YV*dWI;hQSfaiKH;9ybLM; z-YCm6IkSN?2aR{qLsxp%2#i$7woQffzlFsQ1uR?V4DDFcj8a#KPO9Dlp*uIJ|H=?s zs@#d;OF_y2k09)1FyJEyyBdl|I9Vv&P#Y$gjJ~u^cL+F}Y&1|MI1?)IvBfHruy_ou zwO&oHxl_n0U*g0I0-&iAB^SvH-<<9lZIYQB=zs;naL)~1h+;Dn2qUvNp_%R^7(F$y z!!1&N13@r`SS2OFcO;0+B&oR!Eb>^Z ztqrhG3oQ+Ies6@v2~7K5HPv{`*%({^5@L-+g=S zj;&w+-I4mP@An?Ry!-I3)fRrMZC+}4$=!(yjzd|WuJ*=8^_~hm7-H4b)vu~+ZmkEC z#qaS^P8^4jGZ9ZS^oB|AMXYqf#QGRZ@{;D|30`J+nKuyoOkj`!c|Ovl93tRl6B{vs zqb4wJ0*?~#Efag(1mY$zZ2~D1$eDo0q92;roC%y|TawLU(GNWd(DE)`_V{UE^+Ok! zae~qEW6(v%&v>S$u7=)lXASwoVQ~N!pX22PUS8zoWgJ^z0nQ1E&-z8L_@P(*&})9^ zbw6~KIDU>x`Dvzcl2`r)qu*Fn7fJhIKVZ`HynNGul`b>-QZwuRrLtSV1QqlPz3GFp z@fMnKDg6PL;*WXxnh1=~#qg;+Sr=i|c5xje`nead-3)J7z1;B-3yZynqXvZLgUgKn zOvkZSuQ3>#Oj-%YCN=U>Ekp@BLz+ZZ?=ni7@T7%KE7dY>Goq{(vjs-mc1q-{>4AjVrO9l971?&&;8@fG%4o6XQJ!VEW`K&+@G&ONqtC`O(sJN=e3xT72ow+ zfXDfa#PHxr5Ntp-gV=+?ELsrBabVVVo|k?4c?sZ?43yI&0(RX@@O%J7T^0GiayzLC zog}TPYeY@beMgA-dCf5B(o!eGBfc1DMoQznTtxa&{7K}xkorbY`5VZ2lwyLMm6%dm zAh5p3`!Z2@hF_d@lNzW5?;`hc~ zrfAvB=In^+9rcDw9R2Wvn_;0Nb1Zj-7a9%Lfin?|UNw}Kk@^lJdXkP6jjF}0AyFF} z=qyXUi58=R;gu06fHN3OJjctAH>y&QZpASKNV&-TpYrnFHiKID?C1WzjPw;J6h0jS zs@xqHYx(46^7yV0|6fL4*pK#u7aNRpu)@S) zXw4cAzat1i@e$ib9h4p-ls(0YJ}`<>9!0dsk)ZgDpXMT3wtR%x+B3Mf`YnfDK9h?5 zJWaRA)oh39GNFw+m4cRR(mG;x)EcyD2=>*a9^s*!)a*bNo2W}nZ6LwanDS~;tsc&0 z+Urc)qA7!$QYnqu_hM$$sBy3j#DZ6S?!Qi`dyQ=2atlloCdI~x1&DKlBai0|4k z1&pG?Vd58{W7r50Gdhzlu5!}!1E%=<7^2mf|4NhkE;nlQQYW66p&RR!z@=4WUM%>` zz75&D#BTcvSl3%Mni{Yy6M#$1ffge!FtRNp@-xpa%Zfro_X?p%_1j__>J`Tnvfu%V}VR>B_cIAT?F+}1kub@e0b{SAO$c{QU8rsg?kl9 zdMBc`!Qeo63sR7Y795xKC-6W!&o*%*Q6ufe+5xY%6^wrst=Fl2SVPeHM;G7|SU||r zX!DC}zM(eCN!vcX_r|A^n>IyjH6A==1P@|T;n5o~`oU$slST1$NGiJwQ0&GDA?j%8 zjezwL1+qRW>%8F6;M>(B6LOZ6P9EX2bTMG>Xb3Ck*__;oE1g7-hlma(KUz`zt$>@y zwNxmQ%N5*QH)o8vF?&5IA4dsO33r`>x~CB|%@~LxMOg!V@tP3@>PkO`_y^l@G0LHvQH zoL!0&Yt2onT{K!T!bFS*n&Fy(r)n)uNo%5tMR03qW_<)~_LaT}hrli@00VK|2$lUR z#@Kg#=_>i$yNe$1r5(lavu8QMhBFb z{uIE$vGJp$omAa8GRW8O)suRh>E|JM`2IHIhIP|s(u7f{ys-&!GjT*z4g?10s)0KS zd`s6RlB8A@n?PO_2q4zml(NV`y<98^F*r*qUTf%pN>-0VvdgPDrP>fF@y1h@K;<8S z14SZTbs7&XL>RCFwsD$4r2-&yk`vpq!K6P@PNM+en}h@&-a(wcXur+q)Wutcq|GGR zm(SK(0EPBmwGc~h2%R`%!sf#{Kim54+^v7(@5MxkkSQ-zt1an zmu#z^lPYYETedY`v+0q%lRKQv9Gr`}IWL*bj8EsYrwcaV=xepCe%ty~`{9G*2eNs0 ze=c|LaW`K8SpRHqdnXVyZ5YZsY4>zCe`+vU$fcb5!-&?iL=+Hr*jCd?Cw3~Cm~V44 zQ^|~LS-08NnqzLtbqYGt*}kj2vmNiu-f3G1AhKi9*^_YHU5Vb9lW?5gL_F5Jr^oH= zN%Z#Y>gi7G>Fw(5ipSgp-eF5Vth;S1`iL_*>9Df)10%!jNAuY!P`0CQ)JbLr;u90V zf3I!bQwarbCJXHcrk%``o61h@=sPwez`HE#0o&@o3Gf$OZ#a_zQ}Ll>${q9au9Mzh zn9sym-)_s=XIr1Y35sK`6ITVtv$!WSQ$B+Glt8K@aAT*&v!~q5nCEyi1)uI7yy)Jx zK3la+Hk|{NK4=#N@;TeOyBh4E8_Unrx`U%NKzDYk4k`$#Ob)$SF)$G&lsZ95saf0&EH4gC>TCtW*GWs9qEn z`Emt@GjVtBusb(X02h7%2xvLM4`&LVQ>o{BZEIIGS^A(x!$zIhG%N)Oq)@xiX7}6H z-W7@-Q2kXSLJPr$`b4tx9=5IFk6eBka=P(tNZa~cHFP8BXZ!OYem2w$Ab&Z|34Z{V zkb}vA)Xe08Y{qjlfwrQPu2hv)sG!AH(C$%p${ERGRPp5k78Yw@?Cuddw}byZY8&dJ_hm<$A-SI>+2he9%oKGfA23WJkck zk$l|Ek0dAaPJW)~y3x6I+g8^t5CjV-_DPDN6D_Asi>Np_N3~K4QjoGRgr6h_P>ccF z+H(sW3}PncxkplQ)jt$9=tj5LBF3KV&%_On!Vl~;W{$nLLO?JwQ!k`6Z14fw8u$pM zeNZ-s;DA=$XItHVMrfc@#?)xhBBQ<%vms8jt7~9WkLHTln-|?X_8FBocf;L}D z;h3ABO+v}rwap!MGMMA!ck~ToimJqe#vQV){na@0n}eg&Kx|?{Ps$IZoI+vGMCXn^ zzi+c=e|$81J{dG(XO7zVKv~5kUd>AkQ+}Q@;)~ha9cHhKbRDv-1Ad`U`!AYWtca+S zh>qFTXf;IsD8Cp*SC0^5C=U((Fsnk_ux$-iLuq=EwvkH^_lCI?=|5=Wb0by6_K`4k z2~m&R)|dVSRNXs;s7k9gH8Jr>KIyq*Zj7e(tWNwMqV=Q)=tiQZXw7gjtx3L zi3xt)G?sBLzOEZ!0|W*GydvCMqh~<1T&uYo#!k)Wlj*UX8%sK=gK-&$>_jDyOp%tB ztslbdGdtt;EQQ~!*!bhp9L$Hv;4Rj>NpZE9oJQNK1riDsj2SL@$jZ7?m8erI(KU{m zD&n&;rif_>O4CmKQCtrUH;kphj;y>IMx6YVD?z_;Y{2ODs0meyFe=pzh>&%c;%^1n zKuOjCP)gN$NiPMz!+NB}-9jv%6t9O=Rp-oj(^<44Gt+Pzc=&_6Lv*1jgSAK@=fvDN zJsc`6k7;2c>1Ffc^}xrHY^x!yC;G=m@Np5)HYL!IYcoTKX3|b(D4)bMB!!PCLP+)5 zL?TIDP~e$D;-aHL~Wi{E>+5{JKG$|tVNRo?R>K3Rhj4QU#GHYg)hsLr# zYg>24l~V?WlwO-7nbiDnMq4d6e>~~xx;QJ7NxKVcC$d4p!#O4+foyor6a)de*TUg( zN(T$cujp=3C)6OBBA8Y4JXEO4rn7NfK%kVcu|$Ydjub&$&jD0%&>YL=;}27UM0eVq zgpgf{H`~^226_y&;}D>2Z49Si6`OWEp_`0AHe9nQSoD$&p@a}9s)DGPZg1Ign^QcbjGP-8FdR!1#%y2cy$oIyKPH$6GW7cUgann`vPJ znsJ~ZieDdVjmD;(JXSOPEf0?mwesPe0#?AIx@L*$7D9^ z72qGq?dxgZ+1}LwW%4>=g+h?go=#@kkIss;CXlUq*CugHE&iVUW(y6v?G%kBlf6M7)+?(DT-P3uu zQIYBH#oe-%ebS$#(H@>V7ljVwU|wLPwxhCc)FJ(Pdgp*KgGkr*XKjb!z8IOx5oI3TNn&ogH_ z$VZs$4lm#oMcKR|4bXca(LvatsA$hPM^c@v6li<2s(vKZ_DLK2+Fw>j#bq4XA)6~H!pBjfP~%o= zQdmoc`%g~Y`Y>;sJ!dQjT1~pS8v21E4oq?0Nus8Z8pC;z;cSdGod!zUG zgZVx8YFiEX?sNB6lz7aKo%?vShx*^2hw`0^D8D(cJ&Q^#!)q|n?86+M(;JK=T#Rr< zU;oqpzHZs0N9j+%a@sodcdM*J=jr}YKRO8Rz~4RiyN0}Lp*%vY=Oy$zHa0jm@t+_3 zd(*dmKX~!gU&Z!*=j}7Z(J`G(yB%@ty5ln2QF)}H12gBvN!iJh9r}<%2bLx{3ekbN z|IA$5+-|HPDo#YS=i-yr*rEO1yLMZ;?w@Vcb(6n+@BGNg{%HP0(fZCeLZq$`tP#GV zmR0-|ppN2R2`nBN3l8!n>4aO9h!h1sy(Ej0v8?wX?h98}S$spe`5j#aC$)9l8p8*X zkK;q~#}FE}j^KmG6SyC?h7jiOd#iu*zr?9g>pbobWMe)- zy>fl+_#ko$|MOOZqWC*3^&QdaWK;O^jG zFYa-~djZjd{7zg6(F4dHe7Zb=^j>^iyAywebCJSdue8~Iv=cwooJJifNG^&#QrUU| z9jfg<_}5j|e}p>1Z~o=`efQ776{DSf{ViSXoh?x}6U)Z2QZ(KdEc={H+(~6KSo_Vp zg_ip{rqb7xo=nY0fgpnw;!HlXw-B3l(@vo+oumUJoABDO;@j&K((SWdEm8O^lLf6M4RPLHLMF*z5|?&NYU9fH8i&%mDrANnm) z(`^>61vfTB-;J5V_vKxC?3lPb81A;Hm6g%(98MzavzoEvKPIjot6k)ciW{`al!ndy~ILjJ$+fBt)(>*}el?yjz` zuCDIs>D}$X%bb+sIBEQU`l;hQ3dp}X60SWMLh`6d-yG#U-0;$RkJin5X}x`qJ+9n! zQYkp5bohx~M;^XpNpN!45sSJ?%a(K5RB20 zuIM?&BnZZCO*yY-YEkyny*H+ueQm^l8}D+QU93D)PF{4{$#}k1fq$e)+f6$nGN2bu zDV0k{0wKJCN0HAEp!GK=ZnTA5LzFzYPR4oo3w6$X#3P-@d@O&9~<6qYY zFP>27+GxN1zI33)YS0Jb`39YE8uVM}gwbHYLMMC%Q!I4CW-!%4CtL;?U%C0cg^jaL z8e)X12!iFp%zP?Q5*IF&<1q0V}%Ob%!^$)O9ay zARU8hTHNlXInptR=omM2ovw(xxh2)Tw2^cSB09zm-B?$|Ur4v4yO%;y z$1P_N5i)KF$GOrUg-lDPducNX8AODPYeIh$w1@h;sezjrxH(}d)>kG&UzrMhl|=L_ z^>P6qMk4fb6iS6&PMPOHle9)MnhRMk86U7cvx$)S9I0*_Dcf zzpl?8{MKtVy2MIG&?PB-bzPFeMVF|fS?3~@2|EfscpQ4r8S4Q{T^BuIHS3}WtoO6@ zAnuLnAZD+?C<&Zl;7r2H<6a>ddWBTz6%x^k2$A&7QnU@`9DM`rmcBX4!|2z2!^~dD zQ1{Kwrf*o9CpGK7nQHom#d{$`Z7tf3wP1B#$WW|>-DgLz!|>O&(P`PeYWhs7?L(g0 z@z1(P82?hK^qDC{kAJCC#(yt$B1bvNtlT+P8aP@ga_#0^+IRiSE%p>G%U38ADu8Yo* z_jS=ZHs-T*t{d|RTenX02-~~v^T-@fQ;WAXa61FHH*g07cQkM(!av0pjoG2i%nfa3 zW@-WD7hA zgIy3c#xwHwz`E0Q=uP(z)abl4V0E3R%tp|8;eBJeY{I&X1-5Bj#sV9;E@OeMUZ=4@Ieg|==pL{8 zwcxdQ-AfD5#^nqm=ZkUM`8u#X7p3~}Z*t{%2wWf2iw7Uo<(kCFL7pv+zZ+uew{TTj z99R8LU{U=>U`3;S2*0nzktcr-kh4$HP9M;XD5Jb5P`T3XDd(i}UWj&=<|FFwjcBls zJYO9@@5|@*U_XLMxnO@u{$ZSafIKftN7?7&xv}&cJeLz zkG1q$B=k)2(?X9LK>rr$|5QsqBcW%C?+JZY=r17spKIx7CiG14VKBiDhAhf|ob-RG zrJt41GsTB8f*$Gw`X@>M*IN256MClj5FF^6g&y`TJ@~g;`mGXrrud_TezefT+@uHp zUQ54qLeCU`jL^g2gCC5V^x!{g>9G#@=vI30U$ykxCiG14JA}Sd=+7bjzia8YOX!*6j}`iHLVp(N|5Ho9 zeL~L^f4tC75c)yVS8M5aNa&eDwMTzV5_*jK^x&tp^gAZ>O!2#fem$Ybm`)Embv63G zQ$o)af3nc8FZ73yzOI&j=Y*aq{suzdE%cbI(t~a-{VoYTQ~VyG-%#j>NuR2v|6D@P z6yF#6jf8$z(qmV^9=~%FdZzfjLf9e);yCw8Y@i9g~KTYT%+w@?4E&c8ZJyW>W0R5&yzZdBnYU%e#=$S&3 z2l~y09hOrhVF^i8$&dnfcv@n;GB zmO>A$NDnsG((jYdGsWLZ=+RS<=l4iIs+NA=gq|t>HbOs}^zkf^Y=U51q~V-G>CBkyDlM$+BQvS=JJk1#^wODFrhO zh*hJ&qJr5cUpm>}PM5_e|0I@mwHqu2ER`{pmrmu(UgG2FHIERD%%2b%cc1faPzF)X z4NeAFhAyvb%=Txq!7^e!J=b0YS{U}O(XjP-ZE^uMbX7=1WE>w`x7+iQK`lh)J4rQR$m-4lTiE7zy% zI>O1@siGC}6#U9o77DykQcL`90Y1 zdx*f4I#gg<9VRfN778q?g9KJIxxb@s2cP2c*5Djscr9JVHrZ+p!rqnS4(`u2xqB>GwMU(k3 z&K&Q28mTRC^iC?xKrljE60KPgvkJX?F;>j)GV}8cUWo~GTQL2wi!nwr&VGP!l;9^% zgvQ|&2L=uM7?TtG7TWV`?PMS2Pd=4nr`&9p(+2T}-6R?<0@JD@u&7QKSkYu$aZ^TovSbANA%CTf!Bc52g7($> zJCV^tAkh7-MSvMBZSBayuCdXWr`TwiDQPt7h}%Hjg@YLP^F_j=tCkB)sfz`s)g=Nm z>I(vk>Oz4PP42^)W?T4VwI$ZwAF?e~1aW^S-Fa3HY3DMy0l}JdSIkIVV?>}EyEWU9 z%!e?sY%FFKHg=9~BK;{AnLzW zB1Lt*z>3c2&()Ri$-e;Q!Dgl%7$ava>TrV#feJ1{P?`#PiQUhYc1F|IVUP91e4f>{L zq>W)P5rHY8(;}G5wA~pToDht)2ySGW1a~F`<6a()?MiMG2qc(@m_4ZAz1Y(gCLHP+ z{1A+y$ln@g>DQCeU9S3$z?6DQU|KybFr%IkSX564tY~uXxNr>dDb^ipuHKvMoq!;o zt1+t0TrE@aLnOdKfJi#_T%B~ls|i7e)%f2s^T!PS9TVt5hRM>NATUeCwuAO>yw&b! zr9G~CL10QfCorv^7g$u^6Ijt?{yg1YK3VOJZN}WuVCHB9vCT+)6PfP{V?ZzoL99R}BkHsh ztCa#X>K%ba^)rDLP44fm+s3Dn+IBOTD&3D@t!-;*vD%h&WTKZ-VglVZjD~dYL-Fhl zWntSOXWDkKBoi4XFPW701nkwmF?A9?e%$8YNn2d?dx0tS2Z3qzvB0AGt-y*VWBb^T ze2Uwu*F0Ui%u8T4)%%jiRUZmWsSgAe)vp9rbg~boSQelBFNn1*U`|i;8x1ZkL@S+o$RAjwuMizTpFC%R~ZW?O0y8ezRF1Ii*4r`v&tO>Ybe5FGkQxNu|$abut9in zRinU^$_Xs0dVv+4%&lYc$fuEbe1kmx*5)x$!^EDD>tcC~R5#q#zPBmOW zfNAJtmu3`l$DL+lWrWx_8{uu zUuhR`T{;#)JXa^tg=+YJpc8v*iryWnZZ{wy~{mU>*xc(Q-T;V!!z2q}q&Mq$rPIy4ZN zRVN9|sHFluRT5aQ%78MCIR<>IU(Xi#x#}E&DRsWUv^rN{Mtxo&`t>}C6xCS*D>|Q# z;wW%F>Fyl2G4^@t9A_icQN~2#<-AmIIhr$g5@_T-RDej;!{p+XuRucg5L0{@(mqv2 z%u8$frM2{!cOrVGX!;UjZH-FE2AHP4UF(Bu z5#jq)|2jr+Ce6Pdk--}f_%|XL*w76r>e6xOs&@0**pM&BMnK&P|0a;NH`WI?ll;En zTz$V+A3)b~Z+jsb^TK5oL!QMD@ih!hH4J&C(#PydJ>hYrN?))m&Gy!f=lQ{Fodf>B zSG0_8RJY|(brO%}rCw`{od!0zS8;~Glsa7?_9_HsloD7}rwXiSavw7UyPQwf7>n&* zrW5+TJ%ZToC1WfZLeGF89%Bi?NF$_swK4WjR?#pvDnSn+?BUu@&oU~0TAdQ#HR?Xz15J_B>TA)&4VJAHsKu9J%GAL*ZH?% zh?I66k0#uPn2fgmsiW(vZPWufhTyM20$#n}4$wU?rpF8JkeJs~+7I~}hqrW7KK3}g z#^GsAZpgYyi;&P^#%XFfi^5iQeefk@)I-%|budoGfEoPlwU;vucB+p2VK2wQK4`vt zCu^$VCipT@rN>Y~@D)begS#}io1iVYM?c*Q;NOR!slI=9eb=LnjuYIE$DaD&s{jLi z^}z#x!GjE7pmD}PHs}Xh8iI%LIO(XV^$mseN*}D)1A92tGweWaUjt2U>?JRP90rcj@+_a8x|e|k&3 zE%>hkC%hFJ@`k(CykT#Xslb813n03@*FK+bBl4L`kK5$h8gZ4sfifB=<=V@SvZh>1 zuG9{m{KxRrlFJ6)1S|~@b0{?{=RZ!e^zdedM$?tsBXPB^%r~08{5JDb4Uosl)4AJr zo}8O@urDF=B{V}DK)~n^Q-Q<(6#hp;f)PGMjQ^Vf5$VDSc%F(j4nBc$2JeF6%e?g! zM5^8d+VLlm&^^Qy|1dNncnZ(C8Do*p3!X+CEurSO)cMcgDfljenXjXUTJ-mTZUW7- zqWT^P$QeiEZHB||-WVj{_?BIpm_;?(QR z{`c|Nccj}uTaFh@CgbOkF*O2)jmqPwFecely(V`PdnttOLH;>kog>u1$ZKON3_ z{hp`i$~J!?B-p#{c-@DPyVDOj9)$lK;{01&`T!-D{)50WIIz05!8sF;F!)2nA`j=g zTLCvlKSpPj9z}m~zWcVolzL2HT76SsMtw^le4WR2B%_|y5l?+rN3!Y}9jRB}(~$=C zq>kj&cXXstJ*6W}>S>7-)i(rIbp8I}+Mx1D8&tll_?PQA)4;16yo84PKSYo#LlwMk zH~0}Ag6nkrWyay95dUMwZ_@Es7{5iwf5P}}I({+Zu%o2MxX`=897#m5D(YOdjhZ8- zm&VTP_5x}N%^b;IW&9o;|0&}za>T#J_*Zot+AWri_%|4bwPO5D#JlC3Sw@!+Z6^LL z(!f$M{xilO)A6?%|CWxgWE{iY>n@{7q?8s66Ak`i#LtQNfrWU72#7GI_yrLVU5t2_ z2*}I}o+P&Xk)&~t#tYup@yi(hkO9W9*XRbnM4p+LnVf9-Js}ZHfOsx|SXeem1mXXR zF$g8bqSPFvQG?KAXn&0TfMjTR4MLNlnK2f8(1dRWd@!#n!a=Svh-2@|GmSwrw$#9ztY{4 zU}~O4FsUh8BtqBBCVR4Qx870MFD6HPM`1g0iI-=+qp)9MBwM}z029VX9&2sW&*}n$ z66}qbA+TR!Ttos1?oSBDTP38|1AzpS5i=!Bun3MOfdm&M1QRWS7fB$&s|mrR28*I& zGAJaNhnT5jQqH2dnG_N{Z&5T_uWKf+4+;r3K}?rXJ{t-&bQZ0A;_w1T+kY+rmaI&q z{ofh=xrt@}e*mqc{eMYdO8rn^TK!01QGH)vMU(LWCjDMMX)`T*35ST5Ku<0rbKFae z_umpVxs_>mFnAzlyvEm;S1s?8;2$x8ZrM3xK5?VZ(=9un*w}~P)N0c!!n3P>A`tH= z1Qykg1y*!250SDhe6rfIRn(TvHUKB{5R9-7m#kZ^1cBWNa2V1&R}Z6{?wyR+>kd)gpddjmqZAO0C8!AkO&Yk&AJwe00H}~5fTC7ixEO1KwKUnBm%@0 z5kewBTp1xG0>o7jLLxw19U&wF#5EB@B0yj#LGvLIAg+rL5&;4`3z|YAK->@^Bm%^Z z5kewB+!P@s0>sS`LLxw5uGfW11c+NBghZ(Skhc7}a!5$J$3)pNCdw{x{iC5Eik)Gw z>;@BM2bd^DZ=w{siBjBd`7aokitTY_vQ}(~EAy%%%XT(;NL|r5T1g2DtkO&aB zM+k`kaYuxZ2oPV25EA*sFv0><q~+3o%hL2oU#02#KsPtg%WHW!WamLK24Wk8(-`MSV3wNaPfg ziK!+^e3Jixh+HDb|6qiW2oMiN2#EmkwFn^*ARdko5&`0o2qBT^$T||f9wkTw2snOP z-Vy-_BMX7Wj&F_bcPMj4x1_VbDZ??rr9hW1XRsnB&~wV; zltJ=N=LtSi9#hel=~i2?2EqJTRG(;`^`n2B=9y2L=f$PA9@H`!fwdfiA8l9ilqGl8 z1KG#xuun}rRb7vM^cU-T_@wJuQo5e?+>M}3Tru66r&)5+&(Giy{rp|4zCBRi46DAz zl%DsS1*TM!z_c1AFr!8bEUKKqiYDjLR`x%iM(V3E8-ll91S9m7c!7T;L7i{))r)ao z$yVMpq?NXg33TheXSHKC+A&kN0DimK&B(OpKP+(4dpRoKdmi`k#HLBMHHmTPo>EBFx1JTHSP)=J~a{9W+!&To9 zm{N}lEUHHYR&+9tHB(M}ishuwXG2yT6aQg7w@4mW-6}ApRtQY1+XQCR?E;JHW`Pw= z?gzSTKKY-b80{lc|F_mJot|^+kbcb*Vs{6%<%h7YnRta(}5Vn@@hGrfkTVG7-K5$>XY} z0#m9aFs)7!m{BJSEUF~}E1KLd>9YA`@lCru$13}$lE+oA3rwlk1g6!S0*mTZffY^W zmvxzZTBXcwtuj9+d0aJ5U`p*O5T|hj7S%2SE1JwdS(nKtt4#LYcDhWD^4(MNxN0AP zDYdu2wAxExM(r!GsP+(8(d7OzT{fTm`WpFeugh*=*$X6(s}2^JQU?jdSq6a_b%?;C z!dH2;FPhvxMVHMdzpgvOmelwyy z-KAMpe19vx#EM^L#qYM_FI(}ySn-bjr2Oetd~Ykh#EK7E@vE))LstA%EB<>c-ZGHX z)oaCfu;K?=@ro6{F^&&*K=f6w#H|{9ev|0E54+L6{aX6jRlgOOQok3NR=*LLQNI&dR38bfXmbAytq**%Z0CHG zy^}6G4<6OOC6BBABQT{t6_{34ff?ndslVV3w`z!k^DjxL=zM;tE8&x0KzZQbI-~V< z$foojf)VCBCg0F36>+U2gdQe*2|HQ@%}`hdY7b1y8!7=!b5*ks#J;|@RdIMB<(x~p7@t8cY%f{ zzx#U9{*VUi1C{pA(hp~A@Ocf+5m4R$6x|dhE)igUxM9qHo(AU&82k{$%f2opQuXjf zmsj498Hbp{7fH-x3cgT3T_m8@-w1R_?A736ow!^Bbeq?^ogBg`;~h}0Jb)mU`1F(; z{+i`u`Q_oAa>}_D^TIsQzuy3#^Je1PW~Yp=&%7od-=5_L-v`=a6lHp4a0^D65`Z#S z?}ooaymT`qc?tMvtEStkb&55aB$0TMMtVG@V;(HYyo(}YOw=fy|_h_3CCWJ zL3xeCorR1$sj3T-0`eIjUkc`iTWir-AD^%HN)p zQ_7p8X+wWN-En`3uUvDL3Io2YEtcN zQ2l~}BkPrSF{lAS!TYpyt}Byr6a?YF9(Gv7kn?tf)q)GgzH$ zqb3Ln-kDdPXEJUes4Ixt&7js76x7Nq?`}|&1%*N6mG>~Hv4T2*sHnzqf(nS*(@>2U z6ntW@yq7_Z7Szr}?QKw-2nr@9LA49&2%D-!PQKS}RN zP3j8{W=-05L%#C;PAccMdvYYCeL-*t(-TVC_%+k@rj~7o!PQz#^-fO(hazi-n6r3L zq0K1=)j7MP&E)_x!TCIej)$?s%!HdOkO#--PNL)%BJN)f`yl+KoH@w12lJi5d@1>C zw$aNjgDTZ2%p5rJ#&E!G>#Z#ez{qy?V!lC> z57QL#VIoa~?K4-+61R76<{2`1Fj*lFCfvBaXRVr%JhCn*TE2X?$p_ydsSB%;S`neH zi~~c-`zYZPVGhTf1V^K()AM2e=xN-~S>`B-r~KJaSbvdu*fx16J#ACXVZxOV8?ry2 zBm~DGV-J0+jC|X+N}c(n&L1S1YU(V#XD9qm>K~!aqmuf6YghBX)@BUwRpdOeJr;9% znk8l};o#UrUa-}xCY!FX^-I=dfr_GF{8 z>xs_7VdVH&MxCV>;$NkQwTuty`TopviphgH3wjO*l6g+8%@Z**-3?hZuI4~-F$=&l z^D5ZaQo(Vca>MnJ))v4I$O&TFSUaITNvNjb&_*?ZN7um$$2kBzYO5nA2fnsg8rq!T zc$Dc$sGZ{^%zW^hF^A>AHR}*2hC95rsRhk(KTZh>=eNMQe%o!AW)3&O$IjzEQ{)&% z@oaDZF&YM#^Kv-One{Qx;qHhQ&&P~LPTS$BYfe3d^+Cj`=la)@(>K}jkO?^8S0Stx z=hkrQ`IrGA3|7l4D*D^=8ClSF52W&$GknZ|xd06Y2|I)5N82}Tz-^#-XdK=r>MAX1 zJemGu^YFyx4^JsHaFrlzjzplPi&LlPpqk;0)`2$bp9|97`1~haN9RHOH=|?VU&ens zA%>NygQ|INR($?5OA6>{7XMQaHR}>~a>_qXiiRGioWCONAkpJlNP-^sxFHr?>9)mf z0fuC*N8YbDp`VE@V?gHAHi$tv=i*jF#N5i(2z`vjoR6`W3orn30aR6t&?SHaTZy`{ z0=GfUmONV`Y)QF1{@*uziW?H!>{bY>MHmFVLSq(KvM->JrbxY5C)JBkAG1U+@S&cI zxHM%eHOYM{`Lur-GV4WLg3kJ11eU&{{;pt}Q@$e-Yu5SBb55s`7uihtmuq64rM?SJ zHTnYMeh!kLGTQ+(dOMCrS+wJ0(GSnz{~M6|CiRZ9(E1;4L?5A(qW_PPX*W2$htcmZ zACAGTc0_5}a1z#vrGC!U7}tGr!qg47N7CRG;KaWYLH~F|wr@;!70|_|mCv!XaEEoI zz7Evr%N?^}7b?}Y_*bK(;2H#l`j&eCT11S+V4K~1z1o9NU!+s-`ZsVKULn;op-}Hu z2&+XXmhMO9LrK@M$gCT(mHDiKtkT0>g$CV{d_(D1aa*njCED^YNXPNor^M{BStGMY zGqD!-{(_R+@V{b@J~Y_N8TN9eO*e^p#IScT3vE?%K?txbfa7&c>BV4SXB8S-aE(rixe4j}4M6`_3w{ql11vf&fh0!ge{hKJY=9XsvW<<~t9dS#W z71q5`M>M!55k1Re6ck~!- zyDAm|>dIlG*Xxw_vR~Gzy`5A3>-Ki8y}e_(W3kTdWhea??VY=6)aAOpT6gtw@>U4Z zJj-&6&;D)fqiAgYC(dogX5Sd{O7$&4P4g-jY#Q%cz?rNXYV)Y88lrTdtsQC8`SgmATTgF8To z*@4tGR{b?3$@+2)B3rMfu0JLv<_k3itzH*fS3|P4x*{>M zp>~%ZhL-5(s&@qayoGGvt>~8s3#G8I{%uH7_`@*;%OY3buLhssAsCrnxdgZUMy({AO%C<*TCstS% z0emwSU-cH(psh;n;`Ly?oPBgVN*^49tg>-FTFN+pW!!_hyN8(K-vJtU5vDL1*h$`e zZQl7wUZzN1H4m|Tdgj;^M+hHgj%Kuc4v5uGc*X?C=>e}+?q!p=f)NewL#)uz(&68a z2yFb=*yeSpeL-v{+YZeJ5j8fx!~ZIaYLntUH?upa~<3oH)KMg z)2$F%9^4$3-zgqkp%oYP;t}^1?*^S{3m!o6iYq&YBZZ zNfyWv)p@H^723Lk2ibcK`8J94w^_rZwAU(Qvg#erG9F@0Uq&T)LtXVwsKG}wC*?&? zflzx2Q3~~mDTJ{P!FItxA`XC=EN0d=R$D-7jnxHYMvqmd=&_nv!&oiU<~=aU%M?9U zcS9_nnfX;2t9+Pwhm6&wAXYo$IU1{(wP@wUSTRP_$}_M6Sog6?;W*x;20d7J#X$Wj z{!@CeGI8x{x}7_zA!cVpJ^c+gMg;$SPDEloX$+Qxy~ zJ;W3lzM32dP@*)!4i;;39?aA~VYccXVv7G23`buNd~y-8;=#7sJcmRjb`LS7rnYjF zrfX}j&3R~28&i_n3gcq~GJXUDa#n0W^0?Qt!aVD zV9O)RVeE)o*#*exR7UA`Fr?39sNG2%GL#b(GdpESp9VhE=YZ5JnMsG5Z+IuSOfG#J zne`!w$tL4nkUA1xPZH$RguNb@3ciNst}*LQS&C`C$#wwql&`Bw!);g zLYEt|1^J1C9RQZP!Flw`kBGW4#lM?Ma}R@i5vcWn(^+MgI`{@CJElgfl)>rI8s{qD zvFbio%pq&CH?^TEx%N3&2Ely9X=Y!WX&rOU8ZqrSq**zvE%F5xT6k(-Fl|mA2jp3N z)jK_Ely1&_Y|i}%3hU?B4<02qo3^wUCgs;}3m(Ih?7BxH62=ERQH~`gc+_6`n;`8T zVv7G&6ehXa7ZldZPwKDaC*->(bm(upsTc21GLK;_wm>h$qfHii4 z9+m$%Dh08u3v_v+PQkZ<@5!%+@6FfKikF|z(p@cX0rxE5?LWa*oq?dRLCXeyh{ynx zt2tJ#4b=WfL%41Nwvt0BHfqy9)0p;`~OiX_4=R+ z8ozB5IvRg3;a}k&tT5M_>qN&7CAd3I3Ez-5=NV&ctRY{uLJ(U;P)fv zc-{hbgY-`T+I!UXcqaYN4L+iOt^wbLqt1W8b&lv+&V%rn4o9Q~{=AJprP*Fht8?s7={@hXVKg5i@>Ln~_9z#%@#o z+*wH*P}UD>$r^%}fRG*UA0iUB{CX>wMe`%(#s;Y24#U1vzXTGc5M{vo9_X5R`H-OF zfysLz_(wh61o|=u|DA{%J*7U?=qaS;94dMW_mX-FzmIx)I9@%%u0Sn%s7LsYqehPg zWMdywbUJt$>;ykXARYV)B1V&0AYaWLPN;9}&bYizrEet|hbA)tZ|^FE)#8bfCNm%I zm--1xf+oYG5Dt23IOu3v`6*h3ADwv>WNrv&CGGzd(f+*}+zOxCFWDS|?8YzAyau$W z&+CZXL9G7>-$o#sw-OPY8RXjn z-R7*qZd=smtV@@%Z}@08Bo^-~DumTyP}AV7Kg{xY_kAP&Q=NTv=>G|#1NAi8QnZK> zchMv|kf2A@flDFj8}V=Hz`>}!Mh8wo`eC92My@yumz7t-(qB3Dmy{%J z%;&8BO3)+r*DT0;m)ib13YEp@KCg$rIkXs<_?*OCb48TCi|qq6rMEW~d<^r<(-dtf z9&VeMl1*5`4@t0%bu7fBe?&aeH8h`x@@$0u-fbcD1E;|oK&1xcthuYE2u!J|0*k6& zU_~eMusFYm;#2%>iZ1JLUDmHz)+Un2RhtP+sZ9kI)ii+>olJk3W%0@XEg9Y44WYGZ z-JIZeK(Vnk=4Sajt)|+lcLyo{$mTtcv{wJy2!p@iSqA-I5iz5fEu~>UlTcsH55xWh z$XA|~;wWYU4Lf1Acv>`ynGfdUZ!FT(3R}Q_%<>0j)4|`768wY79F_mnHPWo=8nX%m zHL4M#@=4Yx8{r`nI4}vT#WQMZ%=(|ONcjGgcivOW08EYlVvYZ1GT%!7N7u-gp}NL; zbrxt%8}U~9JFM|0bfp_Ifp4V=tHnx9jrD#NB@Lc{Bv~rz9)KD4%AX>kdx$CiQz)#q z2fjoKoUq9GPa|y*n;&-mo0tlx82OpvKSP=&wKO-ACM7gX@xKci|9c1m+^49m9!_tt zBl!kYH?l#U4N}v{2A9`s^{spqI_ri^fCo|`tQOC%X=H<6kCFxhBw3BTg$y(Z15ELs zBa% zZzFG6v|MxQ^C-*66_=F$0OZ3*x*@Rz90Vh@*3ry_VldWom_)j8yBj7EDXCSP3mIwo zMm?8|8!1w1_s*&G%DqLA$N5tQgPXdgcl=JWL6_WM*RXx<~f#`w3CCeoxBQC zZEsq65NgBaDIlyp^&-^6t5Rd?g8wUNGqz3~M5{WjPFeoYu5G=zR{1AtH1?7$b76Nb zC)7XxfsKC}XV zm(4jSiKz=>vhqY^R#Z5hO?S7x6c8uS`tTk}Dw;&H^JB7*(YD3Ya5o>dE74f<%;ZFO zHL~2rGR|cg8d3G`XaB56_H{8#3?}2*U!SyJByFp@f}l-Z3BZ#`$LfyBXEbZLyNBsg zK-K#yOWXjNTN;RL zU7c{y==wh<2aa&S1lYC;p=JGqgGRA_a(^c-A0lBcpTp5g{jDI6R_5%Zm0FNKu7k%q zPDa6;ZIUTM3p2X%QM3t{AfZsd4;czgElsPg*0l=S=UDnd31hq=PCdaGOWRtt4QkbW zDWf^xtZnk-wGkQ9%)2-F_fOchD*%^cEMcoDK4W6f&nzJ_pOv%naFDg+xl=n z>~%uUq=C*<0PVqzgsC2Wq>`lIcsKmttn(kV<7lzpSeej%!vlf+?wGzKZfQbP244RR z+|Y!FfsM2L(du;RGW1ildUPv7M;o%7i}Mw9JR$X!UqsQ4o(Z_ah}qWp5oBSCc%`PR z;dZ3C`WvyVY&Zv%;tD7b;oc{N!4xzun2JEQ-!R8TwwkTr3Kbp!Mi884*?xP4`U^XW z;lKp0PzkHWOQR9Qd|07k%LSVj!KMH`FbZs0M@O4JqYbBogCG7s2S2=R8T`981e<}{ z4JBD~w>c1cbaAp0f&mQ^YfT_gBgDGw4#> z9fbt??u6E&T6orrYFu$e8r+`?W8lT-tUcQpgyfR9@X9%Ba^5t&cO>psMLsc(VJN78 z`CMs=EdWuC75;R{9m9Pbw%x@064pft;C5|+M1AHrLD%~;faSU~8rRd&4fo>tT|j$W z&j)UIDA1kwkH#-hGOnKj4tMI=do)x${Hmrdoa5|pEGUnWarPA?L0vG@V4Tf-2W5Ee z3);6o0R^~xRq82B8PIbb-i^58_>U0x5W&G8A>db~6$du%*Fq>)EYpLMfw~Y<()-88 zk9Y+32Z}9}v%sGEn&f0C)3(X?=QtntiL>Es2z}(`9lx|?lUy9$eT=Yj7s3{($WN&v zC$PSd2~-h6%Ryp3EEGS1)?tw#4EN1R^g7-a(wo)(YbcNlPfA8?qP|XgY~J9i9-3j? zE4*7sXeqAMXdfP#mXchJ_K{U+afJL0R**4VA7pXOE28zn#vIaq03~)0;4bfbKox8S zq03M#J%UHsi5M7__asrC`s&&hc%#QzdTPxiV2Nhv>l%DR1FZUxFQdMxAHM}KV;U99 z4FwevbG>>TA+&DG>!^GkTVfH1K4P6SVTurCI)CoNqpLyHh;6+jB^T=^Kewj04{>fg zU!TWWya$m9cdD_ReNm3yYvn3yWnv7q*5uT;NvpL6UgMk88VqEx!2~X}2(9f_(VCoW zxAx=s7$tz=o^NcQFVg740aJ=2!ii`?ipy>tzW?%Y+Ij7*i+>Q#w5~auZ^}1+cI#+1 z*V4NABJcmb)(vk}7^M|4KdSVSrHZ;IW%p}^532{WLk)=}!#L)GR?!Ej2$cguB+w69 zYc4%*_T&Hg^i$ALhm6Kg*fzN^TlYb)Q z_SIpc9lW2zDeEVY1mS%L05}jOy6&RMCtFFKbkT)x$i4#KjuwdjFfhUgO@51`3quT}iwU&Jgw|4m`A`y<5{1m9wz2x;1Du1>KhP|F@&RA6 z2{T-S=|j%27}%nhpCdLTxGG@~mDM8Q4Yh(&pM6-B9Iv`^KK2ZdN5Aqn-n)5+CJmB? z)VxDW(6Iv8dnY9bd2dK zk(pPC!tO}j?Ak8TO{~awLOIdP7BZaA$OnV+v`fjXz5p zI0Qwz6#rd_oAv0k;KdpnS!XHoEtawI3X(83ehPqhi~B;)Q%G~eSAl5Bn74Boe-|nZ zb>(x6P*>+7Vzi!RQ0reOto(#Iq4mR5nno#%3DkPRYVq<&>zNNF?TV7%DDF%}J{UR^ z?naTnI?0dEfRY>$7-j!}H3ucnkS z%XmX?0u#Si)Zaz-p>MJ4gm-_P=Kd$A^c9FsTWKfdzk@d5#o*&`^L-jz*%$DfKOx@< z((gY*q3HKv0L*b#zf%{{>rX^kKP=rZ2=woB;uFzdQ1ZCNhgf{kMqmk^Sx^Y< z5SN4I15hbP3V`>%Et2$NFhR$H*T0uD%K7?t<8MCSMv1YkF-W|jcY%pzjrXp5fPKDy z-IEY{i!(%AE1oDa{1u2H!w&%Z(Z67H# zUJ{aUb@`YwE1-Fh^e>})xaH;dSU+LmxNYz2eG>?>2$pG{=1f$_>xW0 zODEy0Rf|;E!tTeR$T2K)#%ihc{$WUj&f0zxdd68?J2$+KR;2uenwI*#OV)9gh<^VT zNzm`#1K{0HBN{&k|M%j5M`!_@L+8sV3-@X|=Z)}>!~1_gPF@V=1&5=2nKq6<#Aq3d z=d|$;LjAk7m^OYzK|kkF%b37vgRok>F48jQ!?bZEN}{v$M?9jgj{!!mi(Bw(sq3Ks zqOL!&F0IGcQ#-E*vwA;3-^lQ1q-$32+C`si$?Cb2BTVK_PNHV+q;H}b^VBEIsf7TC z3$~t|JKYNa2_iuroX zD{yy+GctPA9+yHCECP}I5KQEsGHcu7;AmzpAA=C1fn6FLtK$@Ta2(^E0aQ1q{vze? zCc~L=U3+jm(gJL^$q4gLLr#0Qk{LfOV*0yiWGtHm2?)GX&O zVUZ1PzQI&1M(o|1&Nr>yeFw`5g=8%VkU4rAxo6C=po5c;rZ>C!(!*?a^8-B1aFhCX z!f=!8e@KS)=SxiBE(f8t+0A??iJRTB7oh$MgyW50Aje)V#<2Yd{!hTLh2;_#(G5uw zEJY4nFd`4-5+2UbGL5=LKMj_VB1?$YF2@<;_$T98{S5_}Vz^-*EMtIu$XsDm%c$rZ zM}EzxTQy4khZ%G?jB@>t*s3>SMcj}Hg;8#W(E7$vS~W_(aU78YZNvHXys_c@dQVq2 zT2+(EM!Wv6Ss7FWl`#SP6cs|thD&9mwGD4}^Udn-EWiy3)TfMxZ18Gz^24yT)CmK& zu62g21+T77fgERk0sZAxh^ZDA;U&z7wc|qpeh9Xf>Q7(i>Psg>hx5(Lb!BX|MEz62 zjrK^4t`CDQv3}Vw8IHke)POUn0b^RmXqOKx@(VG|;uz)Na(tr#V_g3?>=kSRLqbd_ z!1pAy96#xmG3GsqjK7{8?dstT9;7d%p*r)EB2nvG;6}Y&$aB3VHMh!C0SiTw0wBh^ zRfaX5m<2%eS=2M1*Z$--|F;Dg&vRR>*`RX{?kRUZK;(YRHywD6{%-EQt2~_c9q%wXX;3AhU74ydK<4pBahY99l_ubI2Es(VN30g>MeEfBOWq zC6M{wMUj~QGXU^!Z$`RX#ndmkw##-v z4JNH_C%YrF;XcSN=DepBQ~bWwQsecG?f0Dw>70%KF7%<%FC0gU z^o#wTaquRM@3pZnip??hdm{k$1UAB=<`hcT4RgT6ms>UA0RyI)piPYez(&~~Zb2qlXQsIkt0Q3!daBj3j~&1aWdqBP0SuYlM&p5I6v!DI@|!TZE7Z5IFRtDI^kE zQzYO7lunQc5Lm5gghYVAktmIj2oN|Pr4bSV0_UVOLLxxmn3P6HB&uyn8OtbdXd)9M z9PN45!8v*a4Jjhl9+2n`TF2?JaWqj(K{sC)MFY!7KP8@{-~*6Un^%fAwTS#TiFOXqYYcb zB86xD7OQ;zK@LLJ61`DwX(**XhnxCuLqW+Nnd%3LTVcqL>-3pJBdReVudT_FJ-3Lh zI=JPd5#{sP&FisPO+W!!2lDk@@wieF=-_zreP+VCOTK+8anKQ+vZTq8bQLh?4$#QTJ>aTgrDs7ac zi;;wRQ47mD9mj)B%aNuAPypuVsRV5b>q{OEn6fON9>hMA-a4c;s(RQr@XD7UOZN~{ zd~6c;`F}>hgAfNo?YY8dY3;#ZMiK>Qv9*ZeGT8h7LKIilh=Q76ee{KfOalg!F581i zmnQ_IiJWxFBPU%#J?Zi;wKHJ|!R6hQdlcTJ%=UsU)giNkg>)DF0l!% zNmqKV$=otY$rc*(jd&x}7{3u}ta&3uE(@dbqvZ4xId2-?KkAcKZK_1s&Aahd>9pZZ z^&50ya*b44;i4DmQ|KwKONMB+#=hWDXes^{;D3GmSMWa#|IvK?DdKof?i_>u>$mXR z!`@;I;k3dktvbK21nxKZhY5=2cMc0Pzw7k{&ySxh_Iy($VZ3b)06%k{n+*?!wkbHG z7_8F~`d^@ua}jWvXgeG}Sp4Xh0GOSw;|#(Ib|2sny8hqLiNgvvn&O#$BVo08E9#aT zO_>SBT#jNe6R@h%ia}Dsnw*-YuVhVmKW~{?x#v;}Y zw??=acVz+uSDln+T`(FW0_pEi=d^U^gA+>cxgr->caaQdrg-Rb| zL4mEmzLghI)*%zPpdhqXRMN-M5s~3u9?E1#<51=%YwCpSVNr)qG+lKxsFL|P7BHs& zd`5PWOs9oxt$8!z%7c;nU2F^wuHVh3jYHsuqB=qp+ylgL5vHFV3ezu_TC$H}`bBot zwsYzE4AS0n^HB^xX8_CaYvDAjdB|r50B+O#CxvsOOxaA}ZJLCZ;TPdF8^g~^3r+bZ z82+Z%@Hf>Me)3otogdvkWxYrfHIGH|2$XbrtEf7(pL&nu5Cr-`4+tEG_{G2FID`xj z+?@-8_~i0-q>j~`d3i_Yyli-cD3ul+iQ0ot(7;vpIui}E-G z_QwsGz%4mK%Y_kn=q))j#LzKhVU>;vSwH7w!Zmjd_vzQtHLq~!hW|@lLoTIjHpPiX z_EXdFyhx0#Vp#Dc!fRFJ8dpBc9-{C!5`mUmv|T7yK7R&nG)=^FFm_smAP#%6rXEr z#4kwr_sWRgoAtrd%2^+s4d=4`p6A~Og137)ce+l4)3p)4Yr!AafN$12&S_HKzAVoT z4*gJ69)+<{y0^xlktSrSHc;7c z5op%D2}usP33(`CWj}=2Z{p8i_O09;Y`P&6xCu#UZ9+0%vgMOTZmdn0ALf!D77n3fHLH)_V77|ZE~QtI7_CL~S^ddaOEyZ*dpb?A zI$f!Ji86CEblf~+34-@+JZ(%V=r7gA0yhWiK|_PvR*>Z153HU6xh~rfPw;X$1L_{i zsl!-{?jfwps_daFu*r^lDUiT*8KE@;N)P2^PtmTu110-kMZg_x(Vr?s5bp$HN4p8d znzVWI?6kfa27M>c(d>wmj`$KJCkx}+Mp|FPX2YY!j0?eq5O?qn0{h#*$eF27n)R?LF3?9bU@{QjkK7`zf{%s2Pr zbhoxFhN~`Cw+E}!h^mM80@3GzQZN&(&jSx3ZL3;H(54Ot!14N?9)5FoLexuIU|OxI zoKex(>@?pA;=1qS>7SL zDJMYn#rT*3{~-9$9~ZW%Bf$I0hhYo~ExOu#OX*P*rw1F|P5C*&QqvxIP1&Mndda{(0p%tG8@(+ItxtvuhM_`fMNCA7@`XZszJ1yG7<7f!eCxnO zJY2wsV!k*K;9(l>HJWufjMt2-5#(Ei>(V4_qBUFD&siMiplrGbetSKPhR(cqid|_nChGGiJ zkaseJ7#R(_;+k`CL>?f=3wLFbA+-^jOBWSis&Y(ehm=<#hma#|HxH)hG0>ro1c9;L zxC;4G+V0n70$>8KLMF5vQnB3~@{U-P$?rvFxEF}jYj@SjbD=ZeDSI^R@2257X1L>} z-R8Jh8CxsDR;zX@TJS7&2De&-HX zNdAc53O*h7W*nZ+vGJSWxexe5EIc#4gy&|!w*XOOystAE&wJvz1PC9^InVf-5&k3K z7Xu&ppu9`v`xpW9F~>~?{v<;m;m-hmJ)}qU7l3{uD!dbph3%U@gJ%pfXG1tibJ zKI$f5%4%0 zMg(p#lJGbtq8#(}5*N=awfgb<3Kq?Sizg6P79r#}Z}5ZrR=$UJx*-#|cqX*IUSd9! zgjXWDfh-(?+l81lT3*$mnv*pe;jYGwn{I5afgNGSB zf`G68Tl+HdCO|$^djmroyEo{Abm>FHqdFQQzgJSu-ODzC+%m>bavUqJh*WYlny7y< zgiG~nwdm+|Y0)*R6TylR9lkNIqv-B}O1U8uuwh#vw0lQ zzxHLs)9)_5k38@pzJ&6`2{+u}dpyj!$!M8L4iw_=WJR8D7@LqdK{vKz9Zv*c?fdKb$k?u)EgYO`aUp9FP z5m>a2~MJr787>dvxWhK!i8t&pE_2drLx44ky*D61AYO3x!A zKXe(qKvW@Lkhi(wfb{3{g-MmR;6A_cYO3?n)5EoSgiwUbC(v)$4#~enWN&ZEv=>VfzT&BT1}CD z1}#C#kCGI9u6{fQYjo4yP@F**uN@Fd=!id6+6|TPciFu{vF}uIx=OEFsH61aeZheV)n27{@Kj14l%Tc| zk9X35)!Iv#{1}Aa(u(i#Jl)XKsm&UxPpasO^F@9t(ME?v2A<4m`8MlQi3A;QOUZZh zm`n!>xXyrBS? z`0N0_uQ(9B!tX3JH22&c=!PZG;U>N7IK+Pd1?7EmK$k~AN2e;{ovl+q6yFKwIlP+| zhbwTt=~N`ZOq~H>{0HtK(SKNm_PFINI8-50ac`W2rz2D>E*qWzv$M+n5otoEMhbr- zXx6-c#MFwzirU`CU+9aUW=S&!hVZ1VYU4h%&wTomO#!X^MzE9cM#YcB$~p! z@~KD*WvZ6fE3562P-qDg(Jo1X9ZMTJ4ncG~k}aUMznIxZCcZU`YPhs6a_Yfy1b1a{ zC`(bTT1RmYSuA^;)Y<|Z4$GZ4Ya%jB4*Pr{_5_?K@V_DYonskemWpZl-}TOoaA{#v zFn-QL62{Lt0FB;+>3M9Vd>--OB~-00Kn(Z8z?pZR!T$pE6olqrDt0OufpkI6#s4vI z31|Eo0Wvd7@Iz+45HZu|AHc{(9;zEMK@TsScsLgiwMnbE)Mf_~FOCO)gkr7pB^R** zU7Q>80k@zbq$_0>1pGe3S`e_UykX@Fgq5=qw&44W2I}{v9 zSZY5Qej|3uaI)U5oR6eh_Kg#NF%q(2AX-h{te?N*sIz=+%~e)i1}ei9MqQ3v{R6_q zjY>%1s3Wv|ZQ&{_zBX0@Sb@ckAY~dkg5*Oxf(_aaY|wt*x6lCijASix1F<8FvrQv< zft0J|1@Z%!r%-_L0(A@O)fYi!T7XZ?aEce#L`Vymz)#EwEiX`7P%mB}<^q14FBW1) z2zXVQ(yPN#>?hDVL$&IC$#7tx0McB1kZtZu=B6CpxkMWhn$euPJYf`@$ylrRt-J&! z%L@RG$;2Z>(57)vw)Cc%=KAo;2QTRPmr=ntuIs7y`}S;JC8-cKO^67 zBapLYMsfK%P4hjX;{Ft4ah()5+u5}8N)p#9ZRt($ZFKe$aQN9e%g#RQRbE#D&1teEVk33fR;mKO+{ejF->rgLk2CKz4rh~8){S*u~t)N$H8bg%JS{G9> z=k`!q(~4@%V~Jn8=oXGRmVONrjC%!25v#>+sMENIe62=`A%9W))$-k$ydG! z+PJRKw-y_K&kZqO?TY^n#G^m@rwf5wf`7|DrCE>t(^2$KuK*_YPx)TdbKXN zjq$KGAMo4715@y@4Ii+e9q8}dygtBg9Zq}nO~G$I>(~a+m(z8$6DKO*-7+2b4h5K~ z8BMhove|rAU&zdFn;m^zB82iq3VsH?y?5NkX@|>j3?TRrSECdZc0B;TE%^fK zaLZ%pci#Yv8|v^{#1)Id9d`(=tu%Os5PQ0`{UKQO$eTEG`1Wxbm3V(`@>q6(=g7<) z%B?4!JduwO2AfeG}(39UJl`B2hzG`xo)@JAsF&XSR35sut;I?#bP zm_${*JYDpKq`Vbt#pZ@Vm{fiKl3G^vMl<~t&aFdC@$oXW&mV&T2eHB%g%;OLosX8v zHB+S**F&3^qP%|GkyY+Oq`N)%3G(}S=IlqT563(UUK=#vsJxfYUSFs$eG&O_@E5jU zZyX6-=^BlFtrmNi#=Z{hYP*P!Ax(Ckq=0-D3wdZ*-n;a;O|FD_Yyk@c_)WQ0q>=7u z8s0#@X_24L7TLGzz829(m%qp-2CJpU3sc)?#i@4Z6a05UcZ|=mJgU}9~a zJU&%^18wS^Ji$bEN`-F%5x#{W_!*OU-XQJV2R(&L2t8ea0-&dN0N{+`5jaPNJtx?G zjKFm1Im#k_51b7#7Ss5VtS^B$c$-D8L?GuEhY^A2L%m~()VnVeR&GZ~y(>U&D`(0) z$OP&gVYT>RwEx9?(7T_rNEDDByafok0f3FHdTmsrcaYjW#1wwh(x>i3O!GBiZG3V6 zb}f9}Cgb6=`5L^nmZkQDC8h{V7%60`!7qZp!P{yxbTGra(gvpZZJ-H$$rgJvo`l?C z+1_Jn_$%VF(s_mY@n&xX_O0}HcFAKfN-YgIf6$Kn_`3gbJZpW=9LiCU^mn4krGl08 zK8p80U;r)9S;twGYzk6SNj8Ml#o_w!Lr@1FF-ym)mChzPhb7pT*>7`fcbPY^@Fjqi zSx<(CEbpW%TBY-}hSYq4vs!9AJe+9$yBHnz3gmSN6lx4$^vC({NZ^*@zYB3QZm5Lu zxEarJ^A%v?aWfq|Fwa5`a&Kwxl=37}+-*}}=LVm*by<3 zQ_6g>nC(f)*`pCKIs7Ps+58#d!qf**<*FB^K17_pFct0Le6<%K|Dx*5^{HQz%me6! z0em0%a>ONXR<)AwedJFhN#CrRq+kL=<9>uR+^J{t;f9ak5tpSQZClSY8YzPmVc|XKsN;#in z$1<_f4xFkmzYoC0+JAIAXv1_na_R*ZrrUwV+E}*Z0l0kshr0KUkFv`C$8ToBOfs2- zOnPVuCEq6m8>rFXdU$(sHK@+oLxi19VMdT-<`@MmzW*3}^)I6}HQPvCn+*{*1Ho<2}@ z{*J5y-@=gHum8Y@sU+*_B!!=6mbyAvQo{^3jw#6G0|U6EM!O^P0c)s`>LeN8F-sFJ zNQ&RX8P?FTHT1=sINCw5L!>qlVvBq6xCi|@T!UHX3?wtK_KxEqW?T|tm$=4(5kCTgt>!>oEsw-O=aVmd#FjCv~1g*Vc962tzm{lMW#|rb*aeNY9cHGi#^N#=f&;@ zf*c{@s;JIHO_YF$HVaX?CL=f>Na%GocF5p)F<%;w9pttaUaofP=P2g}fPn5xR+XN9GgO z_|0@csuMY3nw}+LGC_)pXHi>#AJHl?1!-MnsEg_mehW#>qM%h45Z!nojlNVVY1=l< zNjdrgdcn6Ga+90{*qB3ZQE!Vj`=AQ=QK!IqpMtJtpHpLPBRa(>j6^LM6QVlBNN4aA zU^&nO72?9L%e395jZ5{!zXAPrDCGJ$m5FlZpzWN21jO;ADcg1%%G{Lw!59_j_5Y7^ zbPb8Dxzrj(G%9k*v5=G-t>M-y96Xm^#zzqL5O8}m0K4SGPsS+HhFf%9 z(uSmWgfK^dN9eMUdng-5AP0uz5PSmJm?3y9+|&FKVv3MY;_PriMszjwLO?+aLyWRATDR154>Yr!NF+>6x=l=faM(BFuxPW-h|jhspq4dsu{c=JQ;io zGZXP1Tw;7IE-~g@=PN}RCXE)3Zh46BO$g+}YJLaNZ1oz=dW}Z=6|^wKFiiGb zadHbP4cO3^Fgdw31jV&I3TPc629Oicwml`ubeP=wj9JDT1ChNk2nMv40v1&W0~c)~ zlD@-uFgf zh8bi_^|7H@bX#JC8=8p$9%-YkkF-$>@_zZai9#f9+`-{U~5lkv~Tw<(X14x9#N)ipi4 zRUPCRjI^L<=%^frp-)+JBUqqN7?#xvC=`x{iJ;u@j|g>#9^%lvbXBaR@~{lUP_pc+ zpF&GfSjPEP!rcGLGT)3DYIVcai^!?@lJUR)oeZJX(5I^@Kee#{E64C`D)g8>3W z!-WVLo_^YhD9xBjw)eR)>~lj9ub_n?&6s1h2ajd&Pzh#;Q4Dax2GXv4z{ol${>wyL16Sn zTgh5!j7-+y8a{*-^$9+J_>LwCW#-J}n zOaAg+M7J@!&V_T>T8uyC)6TlnzrkaAfEQs9wRjcTgH2~}s)zM9(WYPtQ!#x)U>y7t z(t!TMP+yFAFsV>sRP(o84Tx}oCcL0i(XurqQ$eQ+ zgG7R-T7}BN%`Bq=B{@O_sVFPB&kEE&#smZw20JX@tRxH`qL2w38B!AshozUv;$4?e zw=o%I4^>(}bF1H+arx%Bz)IL~?T|gDr&EsW7QIPs!LGXtcD?SRUQx`t1+)1-qWz4C z#Bq@?TFHbi_4Bl6%X@Mi01B6G!n8l?9`$v_)CXhWNh;f{4-?ty;N8~sM z<&gU35_jk?9L+pyY>H+G`d+;4&T3;)7edgQHS+=?Kol`8af(mHa zsDMdKDsV_Skqgu%Gsy+3x06w#zCb7nJ6$F0o=?B4=6tHm|=Rze8u0;C(jhTL8BqwpsxULWsn&_ z=ul$xURCaK6+DiN!6U&zbutoZ0gjqF52-=i>0=4{OfuHWYqTImq=>FXIaZcF}@HgRm#|8?5 zV&}7&D^`A?EbF0~=-Z72pB~fVr<4p+b|PwnB_B-U^!3?&-5PtArQ4^@e`Bi zQn!98<4wGjS|p{6kIe(a5t4qGRK_Lr-yT zqZvxSy*~3;ACMF3!w`3e3%fGl13(-hXr(}Dq)Hh9@MnKu9&1CfZ$6>b3+*HwihWQ$ z(NCZdi`X}b4D)Qrmk#QrBKFbl$i#rCFS}tn14k!8-q8N_3esViM599|H!7FRTQD%KN9BDN#vq5kzg42^gTS>-ZetOM_F|_41 zX^UwP5qHN&X)CECt1UuXc9823%L1Q2bC zoC3vU3_2mEeq;(1^RWK5`y~?wa0--mN9KoCCuBP0bOdrjTRlrQ5vYgbfRmrPkRD-7 z_E?2r)?pe7;p8WMIpsjxHY7?3ohLsfCjm2N@-ylkX)aTqNm1{985s{*Jrjo|4NE|& zEtq+-kB0#$wbX(6es=+?zYF*^ci=q-ey8{8$)pmrVDl2kQ_xj7L(0{eq1bV^5?N$p zBM=4N_vFtvr6pS_~KxzSH1AL$INCO&1NmGGnk9?qd*v&R|)019I*Y$g+U2X_rW z(u_wt_!WD=hai6AC^+?;ZQ;Sw9IeyZCKw6QbYMX;(Pff82~+d?AOJo4M!1+(1cpd0 z8i*X?wTF5MY4~echgi52l}KtHigu9sFg8es>sWG;V;J7T5Mw&Y_gGtee{B5YqVXA` z;~!^>Ulbev_-K5F==jI$_#0pa0{Z&|exJzitUyqEHPCCL{@%pzMi|| zD?eLki`3$A<<@17iO){*JF=485u8cO$B+R8Ha1{HT06=S8VT^7j?gGrju5obcF zuPw|Am-`^CBRoTFXL!2Unc=BoH|b!bO6NtF(ZswgN%MLeedFdlKrW=NFLLqdZbaJ7 zilnGPJu9T1P<`T!Fmfx534{^>BuwfWxEd)d;jO3U1Ibm{w+tolySRl>z#0E7f#PVU!h z&Vg&i;2$9I=`(RwI0SIW;sCt`L8K(!1&+xmbv|Il{cH#W?}rdb-an(^G*Gq|SISny zBg}+2zcUL~LTCoeg5s>ggoF;)4$}WtWLS_KrhlS=t2X?BZSV<0jN^>*d_N97@jI{` zzI@^ci~SI81E<2-iOcJugrIP4c`ofH$hR`LJPR4OwE(w{5Ch2QY1!Tsj~%+(0u{&PAohRceW6f6Z|P}l+R+S55>5%o59!V~OA#bHg%fg3e< zd}wVuPr;j8ak-{`NSK|*={hpkWiQ59j_Cy883HWvyZ{#7D4eni4d4ht1O>s>z!cgI zoPm@@X}Ic^UJYl$cY<*Ne9@sJ>m6v5d-2C$uEwMGc3FBU{MO-b zlch-qPUAwI2k)~G?{OhL z24IehmW}e5%ADhZ+D>FxIAvkntLf6$#ql5rwl-Q@C!{otE0E2t;-EduLvehIkiHyL zXxq*&OJP0at~ly?GEPWF0x!l1uy&KKqTUI{B_>^AoZyeT*z;)ocks8-53B4n{t%^~ zdqH0p3H?BxBmF>hmvI8i=6vfL%0q;0L3oeXZ+U+NtVhp7W4(8XNkj*(Vkr&U7h1Q6@hZ6}!A)(+QUTI-1 zIS^^&(#O(nfRk$=W=Tw-D znQH_jgpZSCf&i?59!>-tA-snHTmhwR>r#S%1T8x0dWY9Di)?8?ER4&;Xlzqn!poMi zU&719DDWWtB3p|dT6hDDhQ<>}SUzzSInp#hUYb1;A_WVCI|;sTAwToz}coR-2R zyor%ZcoSnT;U$sr{vcTvC*0ol8jA#*=r&)~(tzumd7Fr8?cZL2i+~~L3AizFh(Ns2 zC0#uK;87yoJu;5$L-n2n!p{TMdGXy7<1{Sc1ISbb@aMdMDKD}d8z1nhaT!WsV2Ex;h%U7#VsXSq(HW0^9-%7ZQ-w9*+N`|HJSFVW_H%4xNk9Pcx&-Tc52!6mAqyC*zN)0<{=4`wafr zkJ9CjTR4z3uUC%VvCM~U7oe%M0D&d+Xj_AnOgX`2lOkfG00)sMdxuAI5y2sFB?7x( zobqZn7*FsWBF3c_!R>%NaMgjzA@k~{oNKVWd`K-T_|Q1Z4xYi{TF{lcREzf^c2nGl z|3z`R!AlQX9B&Qirno1;tp2C9>lM7|pvCb{fNqLg@V_W7EBNq1i{n)Q-4yp3_`{*p zPPzK(_cYg{dvFGODZ0Z+VB0T2O|ug^%JSq5)7NShWL=i-&c-QRbCM$g!phYouxntX zbN|S-_<-WY)BGApPk1N$q-Gr$9T$;xOcWp)z|oO*N9NzH8FQvXPOZ#RhGooDfM?MR z{1KjZ;ON#*Og_u;$JMW77DVPqFLi`AA{9?hb|p$ab=eIbak%D?@YtT0o@9zT&!WvN z4)06S*X^}~=W3-S-#i6Do|3*k!VPTD2L6XUC90mpu$?~ukLd@2{ z%`km9fk|=;+yX}@9%w!s+6{7wAiW$4a!MiW{!V4{4D$g&l3MDqze{Sw#Q{b*qC{)L zuHXcFE<#5{ynay76`X4ijdZ?%Es7rV{y)HKQun?1t;9paDntj?s(n~G7U?+AfFpD( zQ7fxEcP5oq<2=;AT|sk5arjb1?%$*#W;AE3CcrCM0rTDD-T*Gvf!jRMK~!q z5l-6Oz?s}VoSP|yg$@Nymiv2f3T!4mDe@9dN~eUAb~kXQbPwk%?n8x><^CR=B94nV zAGt4v;DLmQO<1NZ%8mdTm6`))4QXkIRw?i#%Jv`RNffPMfGfo}7sIfv?ycwQ!>u5+ zf)<7ti$NU55*Xf2N=WcA(5luaBFe)i78p?;SiExO^{kub0<)~bzXE*3{!i&)=?7R9 zVdCsS9g6D8$a+SQeL%Hf4GK6EMy9o~<`R@-tMUd`d5=_?A;waG&^#jV&j|`jl&QFGYEd89!7F!^nTG5y^Tl#^pa>Jx>;Uo z9oFwOdas4eKQ^SAY8Q4{Gf_6TKOX?CsokU1i{?2bw8pBZLC0}jC<9U+K{s_Jty<;z zN}yP!jpJqw5G|u<#cH#Td3uM8yBhog zcOo?1*C(n@`Fx?f;HU4Fa0UATI&jCs8FjQan z$a@)b?I9sgL(OHi@^ap7k)xmEW?i_lCUvq_*66?l(rK_P_DU>~Z4p+oE!y3%t+WW# z2OI`+1?od#uPnEFwiP0nyw^XwBoS>dfbtD0w!Q^D34+kG@d-WcZlF)^9{Ob>sT~UR zzdwkD2xb&|0hG{lO2mTR8rs;$2tDm?pwH+Y`YYKF{vQo(A%Yo&UH~QZ`t;xr$ z<1iFmEOV@9Aqc|a*|0Fj$`NW0{{JxlXeZ2phA|k1&qEBoVq&cwp97x*EyBnHfKaO> z{L{`0DpB9af(qp77cpL75ImPz-a}T;$^Coz!6n=K@L}$?BfNaP0Ly9+jd#K1;{_`* zY_?q{JdpuhvZdXT`J**+z;wvz0cME|;&V~Q0J2_Bp19eM79OBgF#s06%O-ex4>a9m{ld0tZJr5SiRV1+r^*-eEZG#z4vS?upT`9fOe zRWPzJM997E3oJ^{&{fHhP7P0Ky$m4$yaXsml?2QKMbLDP!%B|>sqOL-;P1r6ow$fP z)Y@l2{)cr-H4Z(*&eO=dTwp+abGHQFChuYwcN$MJ2Q(S>6TJ(K4YDNGhcJIl-h~N9 z@UEh1SOe%Y_E>l59m<}6L4v?jFl4jt)A%qM2orI$?k%*Nu7=6`3XUehZ4+IP0o<%h zyCd@#tIskWR`#DoPRUre$6S)EI>^QO*ukOLc%A>>f<>DXfJKmIqcaAW3XPY|uI7Eu zO|9@aVDLTQpR!Ek4!wnXIYN^3406?W7>>{%nbls_b`QyptHQV8!{ku9wyrQVMXR>B z+Ibyodyyz(4B)CT?T*aVF|~CWw=+vw6~2HFYkMilgCV4q<;{ej%FqEg-URg}9?|8P5@o&%+Q#{{lXMMs`{a zQDXQmrqDzKM!%jA%@K$gKt@lyBlFi7G$tA^GD|sM4W$Q&>ov|Nye$flxha#uftL^) z_y5cHK720=Pk^`f(BOBtwvdlQb@3RlzyKo?vN(&x@*&#{^X;9;Nl4~QLOYI_dsCEgKEVWDUnGvR6 zr}huA{W0kXf13HP0srBNk$5KH2W)??;dIyjGSL6e+P{z4{!xHJ8WyQd*Y=+zfa+>7 zO^2)L|DyfR`Th34DOzBY{{EfzpY^-#&$X%U+8>PL|GNDJpJw|zLK<+AZ^PRp>f2eV zY~fuPzLOokWMO9aXtiD zF#tno6K$KkA|#w*PR){wa%{%bY;t63HrYNk8zP70dxqJguyjDky=|@Fu@(z%qE|=e zf52$09>EcbJ%YA*rXJU8!htj<_ZC(s4cd_j^#BKti5>mjK3zWnEEW8pB-B{I)4=Zl z$?cWY)&7@xBpB}^3k>thIw$lHr~-#R)z9c;mL3H!^$@s)_=EXc{_s|q`BslI3}c%m zTQL51{CC37gAv{ypEAc3`0;_*>G(Ef4oU@1<2?;b@8JpRZs>%65Sim%1cA@p4-5Jd zlBrMeKMUnvhJwK8)o7G&04^tIz#AIKcpgM-M@XAY86Ii#cz_;bV3?A^6)}ZvSPLP;v+lHj9+OGAsII*NQSU z<~XQcOg5`Wzylu=&X1TWRm6`qoZK9t;Y0M~2AN zjrzVskAD=7Fyb^kR3j5Q(Re7tZv0t~=8+eh$BJkk3^_<1 zb7S+!kIiFcG!KRxB#*|}Jo?Axu_~GeLptYy$`6Q5usWK6A$nbIU~HTdi0oQI-bgtH6k7oG5t4!gV?rNBP-!4x0*a9 z4Ll{yJne7xwJ|7PH@)e>p@%oMn|p;KU2B4aa;>KB19h$TQx2tH;hc4eo*0*$GPJ;u zA$%UpLHLO>+jvm>(B1( z^{4mo`g`{E`jh&3{qDR(FV2JFs+usAd9*0a;vQ|o02+pRmW%7M#e5znuj_XO{!KFD z@d*f>mJGxiaH}g+GqA5`VDid0>>BmA{YR?60B&`q-I0kgNMEzSbjax!X31Hn=0WhV zF{8C4`TYnjXkmx}zU^-=h>de(G!8>_oWj^RnB;IZ{3`h{glyk$;KYGH42hIu5v?#L z6FA7c4676)3mCZggy?Y@r-A>29Kx7T09KgboMW;_jCX{zhcwb~Lii!jdHFikI8DeIys1Z0`b`516Tlg0XL z5H=qu*@p*7Mt_~`bSoL!1=lNJipW0|Vpk7-Lc=7OIQD=k8&889M(|?9hsPQ;I;V%^ z?7E&OqLwMu2qYk)CC%O4Rhn~$-saeQJ#xxg?+CFw1`=5w6>$$g$a9nMVa{yoE}0U> z%$3z8A7^RCoo3QJSTsgQL%ci*mPyD>eLj6fQ_QSQ7BJy#}6z>v#zlBpuov zndeyQ64N24k<1d$COv6X;vDnXA^md;Pusq@p0==?%O8DaXN=d*KT zg@cxM=w6Q%j{p2BD;(qjxc16N#a)2U&`d+v9Mt+9Xw`PP?YT>6(N#vq0qK*$19 zw8jjMJ)^;J>7HuZ_!KUG9pc@I?^^hG*316@zw7bON4Tk%lkvu^V}3!sTgI3-5QH%X zlB_kxEJM0)@yE_6=;7PJA;a$=)DhC=Tlfx7gV9Bi;xzv+7-dcUmpOC6;C}P5Cb?I(Y9UoC?M%)oxC7q7L{O@h5lj6 zHe5jvwL!n=smpSO+oEM*1p%w0{`L)y5Pi9PK-;D>NI@>GGYp1S4bqZ)GISGUNWC@W zc)>;}tbccrEJCxtjdU*Bm0K|=8JP6J#xd!mpCi-`*U$kDXIMVq@uB4*AJ*jnB@Nf2 zOr;9R`l@8nK<> z)naFcSBl-FgN^M#9PWjni|-*tGsPVS=_mv@zyc5{&%9%VXKVinaR@RR=6U{#P(=C$ zmO1})1K@ZDc$kTfX8-R(-*GftN4oz9V&fuKF~5cx{>EvT>8+dB-mauH^Tt-A=);A zWjf?^DG-Ip2!Uojpow0&rE8_{(QN)0u|>FITXwiw+0`!O*xkYqqYe^d_zQetf7(X? zI(qS>d3^IbgqfwB9#e|W-yOmTY2vZNb-i?Cw3u-ks?m(;BYW9@WGJ{G%4UeM196O< zFap`|lqt3xe3)aIMvp5DJ>EhO`q=42&#xk0GJxHTc1LE8HI@l_Tt+UllvTR#5pLGt z`j}Q@K%f`m*)&e2_@fW2lrGg!agQa=W8L65eLjY$2hbM`DJufO?1 z73The@h4jHBbb=b-%UKbkPw^@4d{HJ$_5zM^|}FUn+V8<0GJ~r$VEkPA1Ywv@*bp9 zRSI2CHi63- z`jM`6Fq*cTT5~3-n_BOZCjI?dn?%-vGA%9K29myGjI$*ICF`J%ynv_#2^uMVft2$1 za7{vc7WPv5G^#qXdV#!6VMgM0WYWhZwChpgfeDS%Ox@J%F1AMK_iKiwAvsw@;^!O} z$&PbewDla{fLs6Wa~bjV6^*08G*j$HX)cF4Har?=!eF_8;rMt zpBmUACfC}I(l>2O#WVCMBRO^GbH*8A_6*JMtv5|eILseP0&Dt0RnA7kp}Sm%4mlcl zd>0&pGXvHSOW_KagR3Jk-dXb(4C8FUHViK3;XexBri{SB$2L#lQmsgLrHrjmpgFce z3+C7gZ*?60KcP&X3%wBkvr)E*@2jXa>2Mrs$h zf*yIJOfupjqNNc+!AY}7M9q}+4(-5INaSrBKu&K)8H{4XP$xOOK#hzqg&NF2@e0xB zvM@AD>@m9mX~S{+4AEoLBAnbRKTCMC_LUk*n8;57gIQCCa)cN`#3@GzY&kwK1VOSd zYbZZ>B+-u#a}3rkf$KzBmTn33-m}>f+od=Lu=mpL$jpyv2|S;OoC3wnmwM{G)P%Xb zfLSpXz(G(KkQ|0GYZZXyg>zT{rbN-y4B+wtZQIU8Y3gLXb5WKTphL6OCf zlUp(kx6aZ>s%n%FoI>i7)St24icoOG(@^vbL&LS8z|kVe(*8EIEudfkj~3Ck?X3|g zdg^N;pgKrHvlTU#J6t!Sof?M0gLmxvvCI~7AcIqSqW(E&a~7ZgcN0mf1bF>Pe6pd= zQsxzxfhS2Z;{ii1;v9*OvU@>?xOHGenQJB=Rt35aRtw?ftOLfbG(r>B!L^UTFNjRB1B~ zDr_kZDzx>Wl9q-i!7u`(a|Oj*IN?~W+shZm6ol1YJ_nyF@V9+KRD>7+<-LivZBUW+ z@|lAQNrjsUuyB$ZSvX0xFPwx3ZhE>eJ-cLtFFj`z^i|fy+a9neEkh~?mvp;~ut|l| zOC8c>G0VW7s~x&8W0s-tH<@K%&(82-ox!OY<3y!;mUa3WGP5m%%olequuG1H^7cvd4w_P1zZzU|i74l?~$&9ESct z#sjg9j0XY%)q=4luH11@cPQ#ILX0{QhE&WX7z3FkUy@{x9fJMUccb~h@u3Cb#R;u^ zgdrs}c7Gik0|p8xT7t`S9E=>2;E-e)g3i-*A0ekMOkPK=lQ`hc1&oqtBWPo?bKyev z9ZWeOLk3W-qHW`KOoyC!B@Q;hF-E`;!j@1Ba<@V8ErxhXzB4Etrn!UN9U_gyDE% z7!IM7_gv)A5fgx0W($c}z>{hSJX@T=)7ISbK)`c!>jrpW@eqM$vA+X62{{V9z)9fQ zB`o0CO8~s|3HyaX2Lhdpz8lc7{&0w(v(VoGoiOTmbR6xqR&Ul&T2i~&X-O;Xrid@l z9W<8iToCbPCa;)W?NAcvS-r}&s2+aL<6nkGa)Qe2JRQ=h8U(+`JNX|2{|t2FF#h)V zl>5n5m^Q%0q8&_85WmAC*#A^T8J<%heD19pMnk{%b~l;r#KVo2r+>3&$K%- z!4g_ zY34o`GEohjD-7A*0uGaD7ZbQ40vrR#^=aECn*=yLXRXoRjM-h=)k|G)6_^1KG^%W` z(?q7vwbb7YG{_#P`h93{^MKpd$Ub*)I7r7DzGF!9GT;L@4-{aGKp&K{g@Vk}=+aca z1l>*Z4j#ptZjZ=Ye*>rw0(l(ghvA!Y6)e%QpMl>S_!pS|IB!OZu=O>UYDd_$($|U* zguVv-g!0zOwt5Adrr2uL1YP0)FH#D*k{BEzMlcB#dwR6CLm5p30R4yv3xkX{oK_Q* zGm+7-pyJ0-m=Jv-qtUXB{Y-*PWbC&jG|5KNLqfY4>WjWhGwL0=1kIAsTJMO+Xe`!t zzgZ9E2Ig7?7f^PH3kbf%MOwp+lpY{?L*I!HZw9SH`Z>nL&}iuZk>2VKZ9;frhwCP! ziLNJ^)CjrITsYz+fy~2Ckmq$+BBvY1&~Ac`CDRRv;BRkqgy_q;2HLhscqTF@;l1tS zctdN1;rMz48!0_v4bE?n_#on&7ez`@QUorFoC$z5(xTo8Mm2o#W=9IQ(-%w`lH|v0zL5u;T-rY$JLBDKvXnS9>Z~mFe@J)7y!aJ z@b#~h51F242_e;^))4gNEFl&f{B7yT%n@P$r=@AzI0e(;EFspKJ@T+L81+Eb0dIsp zZp;Rx$$mTxfhqs^z#O;=&F{JR&Ki+py5_+Znhztm2J~Im}E`hAEQ~(BQOPDip zC-S$Y&mA9r9axj^#tC5UDCQGc27c#(`Uu^_Ynkr7C%4uV3WS?EU- z#}4p$veu92dTZqta|ZnK67l4wy;0D{r8A{VH|| z1ph?J!A!X)&Fgao-x4|k?h*1)QY6G3!ZJvFOy~!;bg?0Si-w3&ZY)GS+nt5&ct1Hp z4>B*a2mxjL__}efKJO;9AT)rZZ6e0V{nt_NE<7<4?U4yG8H&FX-zV63VmbV(@Xv+6 zse_WdY&_AecwFF5LdQ@DNyp=1CAkVg!*kuCd_1!hsuM;BVFv6DibH&$E2GoTW-$ja zI!GBHBg6pkI$Abf$5a-tbBFq)M18=wVKJ~%;0~ep#0Qq}+tEZI z+@XQ+G3wwGSW54Nz%m^Z@>zUfIsN;9;>_sso-j2{o75A?W_W_!ql$?|=4#}@ORifD z9Dy9zTneos&u~e-q(XRqtDd3KDp7A^1w4mF#wj0a%aMu4tJo$zZEcbOKQ77G+Ju;L z8Y?9W;Vs<5f1R`mWFNK(gdXyi3Glb^7Che5g}2zoeexEjr|t<^2TcMYN_5XF*d(RM z%n@P$r4ZUS-okXyJ)^wE?1M^S0dzjfC3k=#2ELSqeIjlskhq83)0qRMCz1u>A(;pZ zl5fp|&>O@RVL?8V2fbtlFj~$;uoEzxNX&G|I^sAW!_mAeEXsgVx-g&!(a4mnFrb?>14^YNCIlmd z;!CwLps2USfUNb98__(8pyxaBxAxk=OWu$s|3P_O_!9OHE4a2n3({R-MuG4HHO4Rdn8OfwAk$@+99#M(8Va*t z^pJhcVF)DkAY$ zmJ(tBr5M__>-hzVZlfJJ6;0OI>x{O7Tnn87O#qk-tOH2EZv?!+F~o2CW=ti}&#V(S zSeN&;DE6h@FkvjJj8rm16P*wWw3uT6NxQtq*d;W< zSdTzGE`X5K614btusJ4o-e-oR!D=V|`3N`1YEq)pI99TdGv_5(L&QEC%ojtTW1oQF zhCY!qb(SMhANak%_8*7@#Xh)$=hGV$WbDGKenQM>F7x zeWVilXah#QP4=X%NE+RKV}`sW%88A5nTe)b+a@-SkfMv|3(JW~j*yViK(`ojWQ4q$ zbo(UW#oG<=;s{CGrj9Tja^jvKz3BzQEXaH317!&5?5PqDDOD3vVbTG|w4%663YMUdD3^>9jp%()@NKb!=N&9A`KTPx z!p;F{GI{VhJvl2q1!p{xeJPg(cv?eVZjqxt!+kvi$D(lzXk?z^0d6@4W%>n@5m4y! z#yV57m+-zTU-CJ}jNIc69VRSWz&!&w2hN1y^9I7GV<9S;Y6IJs8vN~Gfa{qRvwL8Z zrKbpLT?9SBrHxj%z*d47O0vh=hFH;lM~ao|+m9F7@G4_Y6iMz-xePfug>JkSiN#P? zLo+H-ke(K1fG^F!ikRQ{BfG!{7%XHZ5&JSpkDS=o^ymxMqFA%PVI>hyG5XtTz#|+X z25=>jw(T6J(4$YzaV96Gr?aKj_|nl*=^19-IAHlQ^ckE}0Me$r!RFF4eVIk0%@|;2 z@xDwej-1D7K}5@g18u`rcm%f5*J{CZwBT+{zKMbD2r<8#`v@Yk3!8KB(cxMi8FX71 zVqAwh6+5&1aX1YZN#$*yt}kpOf8Q;&!PY(_{q@RHbb~5hBV-+3I{3%|GUFF_o093xY88ap&By)n}@0{KC1#5e{OkK-8BIE-Uu7_@kW zdiIol0I*>z@X7bDpy(1Vj_k@XNGqRf(*x`sI9yQn z7r49P?7b+?%=EJEU}5gb;SrSj+*X80kJ6VX9wHJcJsup1)RzwdiGEBRve|>50d5PP z1~j=0LkDa;a-)Nnd!AtB_Zv^+Tm;P~?jbbqV`NLPdrOO0f+ii7C@CdpXo3~8u_naj zj6;`DOYM?52KK@6zzc*A#TD7keef&z{e*u$;+cJrgc+k>RiF=|AEOVhL=gHQ6e9M) zUdR))1!WUxhqe^I2`gW5#TnWW7zqk0c4v6o$SX|0r66K1e>8KOjd2EI6fws0U1PXI z-103KxFjdaidFGoYZnd6lD%tscUSNmG^Dv_s|7hVpNV`s6&QV;QVPWBcstBo+bP=Y z4tYDY2}oaqWF&)xg6+SZX1yk7vt9 z0|>op5cQ611yc62AaUqZ=mYk74NN?s-;G5U7>6e>sGf|CgTRAcD{dTJGQ4Ee@KGbm z=+i^?c{X8y9thyRf<5U-d@tA-JZ8huU}d<^1ABPxISGD?7N`#(3Ua<>;EY8xr^5aQ z?&BDL@^OLHjEo-$Hg0iVYf@rQIQGYl;)Ye0z8D7YS411Arx59U{3pPTR+TmEpD})O zjOQA59sYm7pCSaPUD6X==5sRs{qaZh;`bL$%i!lyjbrm`T&mw;G|v$8fibVoPggT* z7&>k2!g7~7;jojZy3~tfX`UhG^-Dfmilta?PXbN%@M`R zveX7Kw~V;G%%z-(Eb;g`%wc76PGv9k*Xo?geyXYd)+1c11wI2*(&~ldTxxVJ<2*bq zr?ODJlDr|$rAnp5XGSrHD-!Z+hN=r@O`PRY|Av2>dOe|cd5L;*?x{6 z#(p%98cnkTaZ1$s;Tso?Ri9NnuzIX|9cGDoWaY8*3sqSnVS8%U`>V3!d~+zBowfYr zh2`q6YX?nnsS$GtZIPI{6E3PPS2d;d{~MrksioE5^(|5F^*a{W?m&%PY5_1gRt;S7 zz-pwOlV3AdeT1@HYRxppe-o)DsQikgiV14%jHC*e8Z?SE`U6^IGVnjDV!C=^_WrtR z^?6BlzqxA1yb;4|RDSgpb86JU+AHSNs!!cjb5X~&EOA%@)BfV#khfIz7=MGWMEwRx z3RRxes2`wpsiV;%)0L-wf1OLMn3Z2suU@Xtta7Ot#c#7UK^28+Tmmsrg_f#U$ChMP ztyb@?y<^5|b#(O|GmcWP&D)Eo754ReZNhr>5X=&lUb<__29-KCeQJriV+32FwD{zO zK{X{|)(TKKN-0rW0ZF6kHRJX9Y3j1sz00?#s@eNd_q7YE=1)*x_p6#eQZ1RhsCI%H zGQ4X37Ipvl{lHHjV5wf+F^v$W;u{<3>OY>dP1Vf#?|>53cfbnN!Y8%65ENCSHiHIs z0B@y7oT<*Op4e}P`gp|_z_SRg?^5qh|E}*j>Z_%%!{_g-kDY&k>XoppYJel+}>Mv7gC2T+ZDs@Zc{<;$N&eUhon&0->xTr);EZw-srCwQc^3zyifhG>Pv8j^XGoo_b#=)`o_cWQmrtrSIbKtEpn+-;eVf+Q1ZnLm-_R#*HP1r z$g^IZg4V26ZvvirwRJ^iRjc}L@#oVXQjZU-KfFXeH0+Xv52>1JexJCu_u^`mwK}_B zwaQtU-Or^)4PwvzT4>{~a#Gr`Svi#+brpEXV`|+Jn*XhyKQ&7=ulf(T#Ku+R6|Hk= zZW4|$bK*wufl_I&b;##QHE&7p@-#JbV(;>2)OCxUX%5KF5W-cOo~Jx^dp@x^vWgP?}4fi2k?* zbtEnQGMqi`XV5^2`bGN7v%-ZkrsPyUuija-W-0jX4D!atB;)sJT45f)Kz8Amh4j} zPy2PoQdPX<24Ah3nMdAL0&aYpDw%ju?NXF=gRfovbKcu!)o3+R-A-^J)O6&@Q=8Rq zQ$C)vLA^3JaXQMKOaH&2^bKn6n#AdA9G{>qn$=IsKAz)JKdffxWhFE>EhYb%j{dt; z%>!3#Md_bU`$XM5s$s+@>ghSV5PyH!`>R~)@$uvjyGOh}{|oi+{%@DvrygAVCB~rs z;ASQ2e2fCutHUNfT6DddJmAry`_%ggy-$sweL8yCLr3J-+^0@i{Kbs>RLShrW%cSV z;Gtf*d)ym+O4M!WYcA#KOWHnh`K~FH2A-0A6(aRuTS_1syW$`_ zKvIC~J_s-s%B_;b)sRk?hxckA>@F64P zy=1BCvI#5tR=_n%#UbqB;uU?5fD8LpAT4!FVptwrl${=#qFtrpI(gi6^BGnyuBzp9 z4OCOa|0KF5<2Dn9 zmCc}Qnp!5Vp_1neRWGhDrSzFvWTx!%hakWT!i`22=3NK*H#cG4N z&Mc>ExjIo?R~FG#uQrP7VS#$BI!RoYuVL76knsrF(TnKXq_&FdvK4e~R;P+nWM9#!$;nj~S5W0w=dJ{5fa1#j%p zwOw4#s1$KsEOmGuYxNAfP4M!8>MO37Rx{6!Rey0c%zp0h3iYWP6zA-ps@CU`B0pC{ zp?bxw64%FA->Fdj;sWAY z4w?l;#+@jxAo8pLBpb!`Kq6fO;u^)30YM$q8n;x06t_iONs#?3R6$&mxQ3#0 zfs*5z#dU55U4!FV#I+vUL4`8nwu$RXOe$2UA#taQ>pFD93gwU6DXwbt=?Ya8cZRtB zAay8?J4;+c(Qo%aQ#e~(rRcYNRC(OFIvqBv>`{~A&KK7>^xHkqMJ^QAcc|?iwJdJ8 zxXwZk-=o&VU1G!8`nXFa40AFly&>+8;yThz*XFnz#dSL%+k?$a)T^TxOb#HHbUEbbw3X~>?Ad&K5?E$%TJobSdxZcF!m z++S>ApTs>aVKu!8=jU zEteM@CAP3v9m8$8yzLk%VVZhAbxagjp-}P{j>)!MzH>~q<+9%~-RA0WREcYQe_}#8 zt8FFpaL$%6UH1g%d|Nt?bAh;YZPT2KZ0T~GizN)lae?cR&IWNMN$(ivJV{&wrRP*Q zPZn3XgiUmY#C3+WQKd61uE)hS*?Ee%t`)vC)wxw%Z%NoR=XP-wOL^0sr->^G9A%HH za_$t@r_w6(oM(v32ad8wEpeVDu1s*0J!+-%Y;o-X2iv1IIL{T=C~&Yn>MZB^;<`%u z%2mz_#r2Hzm3y7LbxhFb9`&^I5^-(B#;ZN*4djG)bg7`bdb+x#P z;_nvMt>PLHf3LV|#5FelesT2{S9$z{;unD-5;hxtVT;{+tmaT4be@mVcC zkBZL=gP7`c@xP>ixvd_=-2Ns0FH3Ia1Z+m+P>8 zmyWM(d1>htFpru?bN;d`#`jmPv#){qP2Ei}d(OITe3ttA)Vsz10hmK7p0N2m1D~hs zUxxY5*>Av%rs}Wy40{(o7tCviIj;IsTdJ=j-#S(6?C%jr=c987&D{?l=1|*GG~m)m z{MOmdivG&6f+fCJ@5MKBh(hR#3AOyKQhHTbKCQC{+P;fy-@TB-NAt$me99wvwo4s< zC}F)=dNj8wW}K+cY+L9eglar!oUo43nDw@pM@v3WApi4cpMY;&;)iqX@i)QWj&rt= zh50DXqxm;?ihpQrv=gaf;XEHcjun@~Y^%P;2J?0Bxf-n;Em6mO+2(UQ;yhIUU?f%k ztjFNfI(sinwx&)Sr(PZWm@`hDIF#m7V&3Zy7RISkgFZ{d%qZlySH2e%@%yDAuT{jU z*@ZNnMKsSDO7pA1G&_bcPQLj6Quro(J`nSC|9kkp%Kr(>6a8Pp{O8bbU@jW^JBG?eNEJ)le)u?6klvzgJwpAmdJO#ED=LR+@7MKnC&K5?wKHL!Q9l=E>+Bks*VHYC z>91S`^9Z3^O`p1lZkT%%{GX^e0p{@wHkt7F)PoBiAE(P@K070DhUT6h$*14y%On14 zr}RvXL(FR?j8)(Fz7GCvaW}&p>$)B0Uf%;SZ%TO@GZY^bzgFQ=8^&BZ-lcw=^(uVk z*OH3%x!;wvA4}RVBrVOeJTz}gp}E&bvpj=lW){s+IW))hqWQ1hG~*P_@*Xr>5^4UB zO!FV9G%ru5c}gbDJ=ru*&!riZwC*^5@0GM}DR-%qTP5YbE@|(VwC_sVHc9)mq+N!z zto!%KVN9Q&U`|ssm-V1|Z6eJd(}|7!*>MxIR7Wnqo8tIAFvCNi%*=@{RkA7{rgu>R z%slyCG;0{X3-U(895_E%=u%_E+*bLR)1|ce&$=>%t{F2W5>v;wn<3g5HF9SxKDlLk)2w}p9))o6ST!xnerB-Snjvo85en7dPV!>mca2ByZw zyh((-$Fkdz+s6y;f%)dbpU_(!eV^)-zmC~h`z-ve{9nMg#!OWbx#DX*UqR0M+$`&} zRMO0O>7<#uFX3~oK&VR@DU`X- z{WWs?Dw+9zmdgCkODBJ>OD0Y3PPNk{G1IlgXIJU=`V#qUasLaYoLxpAU9K)`qnFh6 zSx@HNm`3xJ43_m!7R@hmXzH4Vb5|jsqkGfmS+yVET`8oo_PF@Q(6VEK{EbD#T7 z_*)ntw5Lh?HmuYba8|~tdaoO1Y<*eQ>P#>ETeCGJY50!qkI~#3Yx`EZ6seRY_LrC| zgffR#W?^^JFofD+&=5A(mcd`cqjADsPkOFEDo^}mnEl<8ZE5G=y8}FgZMPVu{Fbx~ z=05jIDY0`+8ONpiOZ=aN^VBAdMhkB4NpoEqInT!#>yh?vSpk?GIU8Yq(d$|nJHq%r zy7x|)b;+#J?$onj)})g*7G|>7ugfOYJ=llwSLTxTbU(G%)atprk#>vwQkZPhSZrtv zG}c~)(D_1vUE6}~sPPYK6!~Q55SF{TkW@NAd>$1!iDH1 zV+?_0LOkCJ4FBZ16Ig1jeGF##vL|7NDsO-}1LH5&%%<3Tkmdu7scBxfqi(w`?iW$k zZAql+!Kvhynupu_spjxqsi?8`RpdE%HR)$z;x!YP_D%TMF{w*zto;!FkN4+T&vGqZ z5lZ?LrT0!PK)L@)(<8`N#6$Xin7Z5_aujv}eBY}F%nf}=0}u8g_I_2QaEphmg-=a7 zThq?FG}pSfVmbUZAB*K+ zuf5~(y*8Dm&fhB0UZZ(ZqXmf5wMLppu@$X;X;d z0(^ht7z%Ss#u%83vL?Z7$e9B()Qjd6XB;S}>qw#bLqXM&DOhJ(xdx`a^n=B3tUYlG zX{-_PPn}LqUbmd)7prOZU&B88_eK0(T1IoyB$|EZk*-?398IiN<9NM0HPorx#@f^1 zvw9-)`BZeSeeTYqUSsW?*mJcc z8Otqo`)cWkQd!sWy0LZ~a?_A+aZkecpt;1%U>T9MyjPd3(~=KZ<-Q8O^!Fs4f2@Br z&NSo(nuq!Hcye89L|cfJ>~9=(FzI!uR)=3>w1Fpqcq8|G;)Y9DW=#7~0`;$!?e$zinQaJ8c+ z{A+Sp%FEMe`9SQto43V+aIRp37eL&7PW_$z~_gRV_|xSO@(>ef~7ET z>ah~$38mCl_9fng9NrjD{(trnG78wfjX@_O=H`{nFm)+S%eUkEZt>~6;B9QR{KRV}e-38AZu$~x{`_I4y^Vw>Ef4L%zF zmy$SQp4pS;buxM$miY-vT%GwgOf3QWdo$Gv(RyxIjCpkr#(AWNjG>7d11x=R3Vp`= z=<}>-cDIOTcT;XN;@2gA36s4N^FO(a`C#ww0g2Tj&gm8gym`c!hH#5J1O5}By;AR^ zygIvQ?sT?M0nAuz(7!I3@@i}LFnot|3CX2>D)9ZNqSP156RIO6Zk9IE)M%BvocViK z!Sp5_1v4>~b=P|9_ZjTtd$V*;KNg{N$&?IRvso`)?$NzB!GCuuTc=fR!*@;kPMFk> zC_{GTtLzD5#+Yd{W8qYA933uOjri+jinlqG#Cb?|-F{hV6Kx+{G50fza{2 z4~JQkZf~Pnl-pQKc*6Zw!aRB$!)Zjq@l|3l5f-q-8#}) zuD!mo}j7tT8nS9Pi=9( z0+^8&=EB4;VYc?9CK#((e~(la-)5?(z2snfdXf*+rfKPv^jw$xD{|PKdO2v`-WGcF zX{>ESi`=(58T$@T8A|Lb6K079*C&5|Y(b;@Xk$6%`jG9U%3YEnYNMWutQQq*JGh4RJ}rl`-vrOQiGYYTJ`Cns>}IH+#ie2CrEVP@g{8NuGPH}Z^j1rT=o0Q7vDMK>tu$SGOCNLhQR__C zIsH-w^ie06uCD=0AGO(Z-P&(QCZ2Uwv3Py=s{H%5;^M&#xM;ux%ATmJp{#Dnnf7C%n{Wan(qLZLE^Z)ANt08l?)j zwakn;Mioa~M^%kcv&FStT|E7ys#0~MxVEb0`KMQnQ$L!nq4|HPnxH0*VJ_#ZA1l6c zO;YPj*WX9nQdOxgGF>f`?y8!iipJ`gt&?x5nx>A^uAHyuZONXYei9d>zppx6<&;VZ5_6&2DlX>fn7L4$r(MW1dFCSZ znJwmGHNQ;CL(JTni`6~iV%>+&T&@zzqj}b=`Ql=p6K2+{&Dw>SH8WSMH*7JFR2Ac_ zJXg&;QZ3gm#0<_{r#=wZR`o^k_L(QBN#msiwWH#~nHyD;>DmO>CiQ@JNsSuSbrn{g zm(6TckBEzK-Z*o!%Hc5xrLh!Jt>Pk__sML8! zE7kC67Mv~BSE`lTg_xIAx2mVarSrUA&7E$=yrKGfwM$$q{i*6b>Njz1RmGFut-ebw znGuEMUgfN!>q6BK|5f$9Dnnd^?Az-5)mrUBE-ADAtoDj)t9oo%!K}ZimDNnA^L$!8 zA}$TfUNwCd!)UWu_3YUf0)ddsWC20~ zWMjz!WC#QzVX_dy7GNd`2!bez3yYc|0cBB9a6v#b5>yZs4tNm7V+MkP2U#>OAZkWH zjN%a#7c}Vi)ZHBu_59xNeXr~L?HL8yN|w&&zP*wIrc*qRU(xd z}l4f=4AQ#&yM|)=_a5Y&XuoOuBPO?PI!rPlr(3z*cN^NX?Hv3ONc&SBSGVK+mmIe%cUs6JC_>T&i1 zYt&SBYA~rInmRPVEdXW(CH>X!*gCL{k&Q%ZT;SR?;cQ#Iv=LK9b}sQp`2?oY~JQ%w`X z3e7xRQ^zJmlIp3cNu}083#Tt6VO*+8dy(p|srh;R3jO#XP2H83PHMQOhLj|k{5gGj zALZ;NZDRv?v8w2p=ky)n3z~20(Bq`O)zs{v!K8lHRABmX7DWHLRii|v2a_^uN{Kzr zf_aFhf@6b8b=TC5bYz9_SWPXZBP)a_X=)q!LV3ETULjv7&ruax7RHxozGsGHnZkIj zruGiG6cNVPXli}jaTdcY2b>blHJ5ncF;n);E-Meu)WDkwgf z)EkAvENdBFsE|g}O zy7ON&CB|Q(pI=cbFrMgb51yha;hq=ss$zW;F+#Ybssd;efk4pkva zPd-oc6~!Jg^yCXQH6>P0-}t^uQ;(4^nlIPX)8vch4`?bi`-mZiKdPx=*?RhZ_EVZ# zK;^9bSxwzd<*aa}A|VKTp2Rh09{n11|uDD{fakH7gx&f5z6@sB#??9any$S7*g{(Jxwu5pF^`4}km zJ|u-Vbjq2|gPpRRn{BtOEllTAp=3Ev;Xr<;s&I80!keJf8Zx=Ri;Cfjx4STt*Q<(h z{#G!GUxaeAl>V)SqdC1oNPljYQ>_~}mN#hX@c6KC4$i7n-}O`bjVs|5n#!pjIXDRX<7V(~Gi6S-ot6CASyIU!JuWU?jStn-gE_y9yPZ$c)WcNnPQD3B ztzi*=PW4d@oW8Ta6RMVd!S#+syw?pfW-S|TXbxDyM{4RHfdP)C+&owHoeYU^+|9c~ z$sTcb50BM+egQF#dw2?zkH72UtcG8jC(HTxsq|!0zd@;OadXR!(x*mo^H8W-_B~H< zxOsx63JnR4WqgRH)>{%BwS26m-Uv)^)bZ(>`ZOfLv79f^R9<+3V+F6%RCGjw<6i!l zrgFL^IPT-yH8rw(f@39rT~ojHNN}v;Cp6{95*(}f+4h|J1jicwqo!7w6CC$*^L*Kd zT6QfU!SMi(($v-91jmDXfTp&FB{@rCBg9!pP;F|T@xG+^VynuI5NSpjxW*F zQfkY3{(z=JXj~rQ9!~=)RxUWQ&Y#OEl=`7P0ggXJjG`~se7}Y&xKO=W<6i1JKY7$b=~Mf#m&ZfZ zvb?~2$6lV($@dB$srf$Y(j4##zf)6R^k@#)$G3IL`6}O~`8EbN2fWJP=;V8izoq#$ zg)|4e#^3Mc+s{AIe0{nNaqQ<|w{^7abshy(%MwHK9k26-PQHKf9hz@Hjo`m{QzzdW z{HW%ur}2D)pYG&q6W2O@0DOo%!D4 z_brkwQ)j-n_~V+Av*Ka?%Hj^+VSZRs(szWfS=-?|!Z&G3`rhUl>pFaI^O2g8zJK!} zk97F{&Btg;`i}B1AMNlR{pwyT? zuG`WP)5k-h)R>?1?oevX&-oBdH5s=%&hkP{MFebjoa1#+YRvQe%rhM^&-06#l6&(D zzII24?+ae9De1ew?|iYtcY)uhDe3!?uiw?-`;s?kO8Q!P?(Pm>D|bMtN61%v3Y6NT zuXr_7E%R6QWq-vtY2{>;ulbHW9Z|mKuR^I&F7h{_)F>DEF{oNv@_eu3BL6^BeT{n^ z|KTlA>PURcckbVoKB`7uouryL7t;~!{h zTFSJ{HvXBWWN)wW3!0L>y~ZzT>NGtq{lMTUVrkXK-IF}4JRB-eAmgxMVsckpgZN@qWDd9|H-|nh~T3gds7is zO-Y|lbbF`6N2?EOO8WF-{d*lgy?7c*J+cg9J5((@M)QP0G-~R3nkS4x*DTA`vhVzN zIgFwVN)RFKL{h`#_{lp-sTGmILcle26nwn=g z@9-D-opJ^Uhvv%*_>Ut%%+%Cv!T)gtilv=$28lY&Cwmkm9*0s}9V`yFcC;*5e55Ja z>JYK#Qim@@9Dq_=9V(7NsjUtbA3@c!M#D8ns5pFCmQ#IU;slgDQ}Z2RBI-LermP`c zEWOfEL%3K4rPk0zJPM^o=^~zls%4c*P*E50tft;K1Qm4^J2mx&C8a1*G(oBT>n`^E z)KNosaR5rKp@%pIrPk0xd;nF;)+nQkdWdtKaz=?uopMHrpS7I+N_i2ji~qB%p_bK# zPb-QR;hLHrQC$=xqBOO%+l@t5(O*+_-ESXC+28M zUP=cO)oWuPv0hWsmnhcI zf1cZYiDHwcq_3|q={kIUMFf;Of=MD4N^NzL=nGZLeh|$8Nn)_39_n^iQL-4WsSVxh ziu#MunyOUpa10PdntI=`Zb*ulq^UP7tBX?U2U2D2G&2M~T$CQxmT>M6<)4{B;y z)JsJJ#gm#UV*e}}B%aaKO#OkP!Qw?IwVgx6E1K`dE-w`g5ep4!Ol#K@MVX?E{_8GB zLiRmNgj-Z4pKr57yr!ftTQmlC__DVzQ>1=nNPwT$;LzT`C$Q=4tA8%TGlGVxgw`23;x|EAG)$LFlET zLa|CyFNR+#8Ydpn)XIoUMGjG~seyr)ii*VZn(7gvFD@3ZXlgePEG`irY0BRaSX?Un zyURY*GM6Q)c)S>3#FF3UaZ!9ADAx{ zT`ykl%JeiKr}(A z$Mr2DDn{0>&V09sWKGHA`c|Qf>+szw!Zjs*3&qHU4&OpirYY&WP0&ZRf5f~^RA@^2 zZWlZ0`^4?_-7Xq6C4F~@d3hbaJLp$JR3&|Pik7h*zB|PwO-bJ(5l%ml+#Yk0h}V?# zEf#l9?eHxY_i0M{mWaovb@-NuXP{P?!g7uUEEO+mDk^7M=2Ed&QzPj!|GUH+nkuHx z{O=NnRfRjbyTu78_3Xb}ocWXQ9F)9@o_E|WE^4aP@>uab;yX?KQ+cwuM*PyLoLlIo z%XZeXCgZckZczlK9-p;hqULiO_Y~KPo1xTwTPOC@*t4L+cduyDl=R&vtn~X_?J@5YDVmbLl_L4}4&O?VqbccI zCFb4H;aer-j}Xcz(zjY z-QjycT+)>EJt&^LufzACctumvw^k&t>F});IhvBbheX4J9X`3jCB5sMCfCFqL8#FC zcC;8c3(S%0Zr-0M*QK;($UJ-qbrpZ~&7Ufn|7wcNzajUp-VgGn_d}cX($ZKI?N*BO z=Li$+GLOuuAg7L9q4;`MPWcTi{BQYxM+=Wq9)&#|En|L*{5y%$+lrLm$eu3!Yc2Bs zHG=&!S=tNC!NG!k5+M1X?K00N0l~ov`z%;`a}lSwT=JL4%2H?gNv>KR9xL;!dA1Zv z{>^0Bs$jCy%rHz^K8%%?w_MtQk^Lx*m7uq5WuBjMWz1$80eVXn-=UEC=nb2gj{JY+ zoT+v79V&CGT>f`RVc%05bZ7(puWo=GjdewGG-P{BEQjKAdXrVwzJpNF>Xi}b+uzhe zL2t!%#FWwKJ7W~NbIkUfYMVRP{xNdOQk>pTlF?+$&b3gl(N39D{cDe=qk0wAn_8=* z_klW&w|fW5_N#TtqfyYi8ZxJ}>omJRjTL>%8>qHNZG)QsEtlMzSu@-FMY9NcdaLuV zmd<_mQA^r${=FB<4B3XyFxrB~S3JHof~o|3zyr^##WBVYEcHN6%Uu(ksVP(7Sgsrfh%vSar1Le|@xY zdV5SpQ2TBykrCuR)oWv}?q8XwegBrw%plmq{X5$Bzkf_xDr8w%i^75?cC=O=`GU>H zp8b1`YG33Tr~b9?FM0eZ^!8XsoX)-e6lX!lcnVgJefi;jJ7v9pKay>9<|^zp99@;eESfIdtDJ zTDEgJ%@Tsq(mceaw=)W+&IUJAj|9ExCHIJW{K&aTu&}?J=?Z3GdSh~Adz|)pZx!uF z9h;8xRL)WK<{ursM&=7t=+PlBDBv-1E_a`-m=mvp>a@qeUaMMp1%qCmr?uw zXG_NjD{LvXU+p!0e?L#g5B{U9S`WP^gqU}!EvK`leZGppT-TfOx6gPpDFS`x2S>ku+!i{}aDT zmM#A~F0)Jf-%*`c&A5w@Gj03${O|M0GXRo>+ z)jjpU#;w@nTXJPf`?XBuON}c;FOZ`1(yG6UE=gOFB?P~lNV&yTmeprWn+26+e zf2XxFPkUXRTiCg*8o9IO|Erl+9S1oAg5KBZ=u79i{-^$`c{;awAdLyF4h2+8b&k_{ z%%#1(*PX{--GeHsWmnOa{g0eDb32c-T8oOE?f(<++#Z#`PBTw?eDz4{Ja5Q0w|nJG z)qcKo&iM$=CKct?fu6r`9R9to&K9+ga;BnpI+4F~uhpEYMLiePaqiq6wbz|T^?%}> zN(hX^!`tI=krA!$^2qji{$MsS%!A1?fE~A|NoYMFxy4zBJX0?Q5s9BkkU9x z4oanz$|#jnnn-C9rOA}0P^zFboze_SE=rY@uBSAc(i}>2Db1sFGo=NT7E-#M(qc+W zC@rOQ52YGPZc582)lynP>0U|?Q(8x9J*7t|Jxb{@N{>_8K?4T(D&0bfp%a4umm_6 z=mO3m)KlMN*_DJ{=xrd$HxTxqIwc+3*Tj1pd2VlXI{c!x(}DDKl)?! zM}MsT=#SMO{jvI^KURNSgvjCS{b_g6n%eJ`-^-%Fqrl_Ai~K6P6Bl2(Mv5_9U^N4W&V0X*NO#vMe?KKh+$>^4$+J@H>1tXX!99Z&cJd8mJ&mZ{WvQzTuff2c)9#vzn2FM zd`vk4ev;Z2OyBldnEQ%yiiZ&TfUUsGz!F2o%zsgS`L;lbA$g`(DKWfW@uuQsn+6kdHw<1-MZ(qo0Yjrv< z$6v?g`0KbFe{P&qG*r)xzc{YbM~Vj~Y|{HvEo+6@IH39^eYnP*85i_Xd{2>;z5_jc zhFc`_JL#R!ZM^S@(S{IXY|J=Aq%L%3sUgxhKdZuEGrsGZW$@@<&7kuOc_MXk1THIm zM?Y@3teEqT6Mvg}TEOM!%ZWyLoFp21(t9C^#t~B`K1uIhBpQbmNqly&#HZ-(2#+qy z`I^C_J2I__a4)^#>Cvrry<8A{t#*e4^40huM*O!J3 zXu}3^py(UJ22nFCm=zfNrGHPnF77H}&~?XIq_H-;!cb!TZTw(khSvAMs&Zov`aTkU zFGSxjiN1l6RigMhD57Dz6;T6kZ(Pt zDj!5=m==jBdS76U@$sx-#E;SY0gHru^I(yX?-48#@{NHC{=?vLrVvGbn?FR6-`fvS z3`RNSwamCe~MOH7y0Nqhs%PQyq@IIy&}( z%xhpTq5sL!bse}K$LD(FS&!p$vT`9~rg^d=kJ!n|Z$lTDHzCg^n8D%HTnQj*iO#VOm3L>-tbu>O}g+DMH7zK<6CId5oIlz&?LSPxN0$2^4 z2V4kT3akUJ0j>vb0xr-U$UhdajenEV98hBTG3ONFCt05cgoxq8j~F7wcV%A%G$5zh zxN7*X0cNAV`qzL|vJ4MQ#SD;2`!|?Le87?@mZUoZ6UC#+jzGIq07?>&K zzRncUJ+n-iB8{-XB;VjJFuk3zG|(3Q zn==xF+@^n2(&voWBhA<&&DbLz{_RkE&;qKZAZQ=|F}*nGBJ)iv4@xy2kC_&6^5hY`IO$l&4_SDjexS!TOMpT zeqyT%Udbb^Hw2%fJP!t6M4pRk9wBEcmvN1KXKGS%fQc#KINX{u0BUX(K`wa#=PC zIWvtvTPKBhP48q>g~Y>RHcoS14c>wH$%@RGtjIl^p?RxZ9|y`aJ($kuG0rDKGL2?i zeaJrH8vRvB4r&~U8b_i=ujohTyH`A05f)mgd^vcQp-_=?zE@1E?j2eNOBpOD#qKHD zp(jOCnZ({D*`Z$X7{)+_6$Wrk%Mz1IA&NL@5t!M$95=g-UKcM>y)@_>e}$5us6frtl8(3 zy&irB_H(eGgZ(06UIDgg@tqmbU34^~((zz6S3A0J^HJv=UHo-8Ds(t1bn>W(LLRd* zqUxXL`M6woP;qU<$xtecW|(sj;JfiUS%Zm_{2LI zEfHRtpCQ9Smru6E_o{K&3lTKoaAX{CgO!n`B&@zPRfVkv8(~u94;y z>8rYy7}6#_*wv*pTVL%u52Y5OR2$Cdg($mHxwqeit}B%}Grs9+GjFke-&Nj~l?S)s zOmD-P-lm=D_0vXoGaIu9OzT!+sG2VCRpN$K7%HgEcHt8PVmEb5RO+W4?6!d{mk1A! z|GC>z)LVyo>rmrEN-)iV8YS#X>+klije@F62&x);!B)0}KxnbNQOE;>&Kb>D$f@{V+Lcb`ZZa$EO3 zXv2QQ*Kv7;+K*aV#j`P6yX&~TTIsmFTIuiv*M$5>k^d-i+RYa-@_Kmq=Q&v>4_8KA zir9xewNJTWn4`x&rO+<1XT^jbr^LGKiXP2~a0U_1Ai_Rn3f;-eeX+X7O6943vkV31 z14YkJ%#Vk^(4)j0Uj2_AHuLS)BRvw8=$YU4kWXhxJ?x>TB zJfcqG&hVrnzm0oRk>9O#nP<}Y_~;y59p%G0=%aHmJgCI@L)zh}R^%_^a{f6-<3sSPbaJjcr^~PE($h}YrXD@7z`K&)nVH{nfmxqZ)3XhGN~f3S zj!rMnoqeKuJdgI*vok+jlP1J+@_QJo$GE(sf^ClEXQp}44GnR z&r1=RLVnLUQ}iKj)5*KsOd;Pix9QaDsXUu(I{CblDdbzxiMp_y_Y6K$M$YJNHr?lC zE287|a$db6yoU$4q0*z&1LU(s^$n1{^r>a&6N3q6ULzbI}9-C%xtxWpCC<1vvUDIV=^!fIT(i=jDw9w^b53>=&lrpTjd=^jCCa1Fj6nCDU)@^Az9X|Tz&&u zUYl|$Po=ZKTBxt7YBQJV4Vm;LMOWc-aZ3@|tvis>8n*}ex0&zExHn#&efP(o5+78) z62Bj{G@+Iz)Ur+Yp3@h96#1LA8o$geH=Y4M2V9D4^gi1I@oNw|oXTtQ1t z;u!YoPEWow;iN8Pggc>4|8M(438%z~k*_7_4Dx97H^`&Z-yo0DQ(|7xg@h7=y!W%? z4$Us)x$M)G(S1TVJuk=hTA-8ja}@D`y`l_q9ie!GT)}4>PjgQ1b;SI&qpDZ3K|bSb z^q6Orta;P zLy_0g^&(-!v0kmZF;wqJc;m@F$>wEytMt7u>vUyS(`DV(T)*B|%|+JG-rVy3xTM}8 zmbC7vgxj(P_cmLU)Sz_LeP08<9=M6{VxJIW z%kXjZbe!c3ON_MSkNA@Ky0qj(v+?_B1y+x4MQT=}%_7fVkM09npUR`VV9ia;G`?M3 znkY+6NldhCAN{t)t{8_cO{~KlTZehI4)baq=GASye%iIr27^4dchK2K_Xf1D8$x$b z8-7aMVc1h7`tC8vqkWG-ekQOVE%YewRwVXq!ahBUeR>r8wAmofZpxAnb(IBkg2hB*7g)d8tVZoQmW_bH(j1xYUbcV$IM`;=+J zvaC9z`q>2^dc?4eJ}Zz<5TP?&N!xIL6@sgPKRs>9Cq#?$wxke6zP}!#$T!YI6#2fo zOE2$#LKL}Hg-b7Ivk*njW+94vW8I|>?f;4rqS*RBNcCRnC%HmA+$is+!j1AtHry!h zrlMepf+Y%;RLknjx02$G@)%2|HXN{I80Dzupgkjva`q@R%JDoU78MOno~+1g-(*Ez z?yV9p>d$PRZmj^ds&o*Gs zHek=D>OYP7D0v6+?|^p)ygT5P*J%1Y);K?Z9DV+C+BVK`lJN1X*uSxk&jBSk|LG>(QR|XwNB8R~ndl1$DKdt~RwU zak_k1s?LO&(}bDRB*#D8q#oOY#w<=PGs-hA3YK`ncT#=&;guIslZkVd0lU1bk4)qA zbD8$E97G$5Xd@AA8|Lk8y6Z}AOOw}#d(yV)&N>^?eEQX~N7D*T@`x=n$s@J`WvfxP z8fE7p{z9PHXs9}t&grb@%uRg{X526=HY8)INuI4&4f2WGWqc>2Z$_keXy9>HhsbLX zS!a}=zpbQFy-=zoV?8|+Rb_0V8fovN#s<{*kY$4FnT*SHyzL;oZN{FA4VF)1Udz~E z`pp(FFvK9g2^$IAq4oW>nDFpD;QP`3CPZsOw4)|DdPk`xZ}x3QOPWn`mXn`nng@9- zr|t6xok1JUsikn7oI_cgVOQyAWL7@SD^u_2V+*QU>GGmVc~KN!4?my9?T;1e_YS%<`< zR5D5>qf~}jo@*Iqd9JmJJ^k+<;uH3f>xOK?jI;?e(k7ab>|zsUq$47#Br-Ec_Zr>N z<>0&29H$xZj>|UNc5!wXZ=1U z$DU=~r(91nPn1#4s8L4Q{wSlIQC*m$TzWaryYydEjV`@9|IZ)sWLBi{_w=n<;fNV2 zHcvQ|wabz`ZE;tZzGuZdS%nzAGSpj!8ZV;8inuMx?n>{!3-3hY^*c=x*O zY_DG4iLJ-EzlJ`&wA1|gd)dhBaI`QSEeuBstF=-`uUnSAjYk*m%RXX$t+X+F9%^5R z+83&|<9`QcgFdG<+eH8Ko0Vgt{};_Ayp!Eb--7Y5M+jdA9%7G?{S3P;CzRG#nnm_j z{zy(ByCN17GG#GYUeG;~V`I0nU}pmTSI{=5H!aUGkbWE8k7Cjaa{H; z0rp(j?XZu6#RXo)S5w(4SY`s{BWa!hR5zL%<_spH+Dl_Oq~@2ey*^j!LGJJyLY&iw=DOnsu+S)sO0`k68>4dim-YVE<0_Tu-Uu7-qwXoC?hL$b|UrF8*m0RH5 z0?St5HnM+Oc@Xx4up9y&A^Vq=XJJ1J%Xwfc*{@bAdh|<=egVz;*O;j)4)!=$5`c+> zpN~%@ueE9vECs~-RXO!?e5#PY3ig@6Ipi&?s)4r_-dfm~16PuFW>r1BTj1RS`&QsK z^4?z62=76755j&3c!az)RcB#83(I+6E8*wkTgkh+N->~c2J{PPHoV3jt%`#^4weL9 zA|dsQyie0lKgd#fu#W;37&fv&Xz8Ga$y;1haYNr>uU`ZrPSalvOqhKi@OaE%8aUC00S_ONxaU(nDnhEb5@(!=AA@AJMT6mYkzLM;d ztLw@BV(Avxx5B=S?6a#6!g2_fBV?(mJ_}1L@kgr_lWd{cL}OJQ2Oe+2*plV->O5Eq zh##)50K4i$H~uUvtz-$E zsnEhmRJ-{#7B@2vJdya2nYre5tY~~5ECpm4J+lgY4)M~Nwcsm>J7?BY&c)-mz;Y0H z7RW5?*tYSCMaGW<<^c;VvXs-ZnvNeQ<@s`a73?*{ZDqCKTYv|FXMu{JjAQj%$EwTX z!1I7rz#6izE2{F%LA|Sm&Zmeu%7JQCTxMdk@%iw*7a(U9Yrr~o`-C`H;$g`HuL9PP_t^=x;9GzPfh}ZzZ^Bt{B@pca<_4}~ zy7D~mDqtnYFC@-5&8fh}b5 zpLiBr2}VnRxxsS3TzZ#sTfXJfI6$1#|;z zf%PG>uKJL5?9GWDShfJYz=J>^@GOvr%32hl4HyTs1M`5+&~@y`i7xOepc_~V^Z>U2 z8!2bpBro_upf6OOcRuj5Ko%xzz1=@gdz+9jmm1l9oSfsMcxAnPXUW!+>1E4URr z7w81mz+MAh4_*)62y6jbBV~kK;K)ch!#Kfffc3yeU<;7Z5_VK0Fc(+@tOqs%TY#(w zasq8VHnMnUF8IhEs0F+RSPyIjwg6ca$^vtNPGCLI6D5!2MsP293%C!Q^^|3K&y6g_ zX#>U+e(TDGeI)T;T~6>Cpt~o|3vds4bDWLfUf5f}Su|os%a~Sh8+b0b9oz}-0@PfAxpH<~YBv4#pexC`h8dVr1OJyGEW_W^kV`j{Xi*nuus+`#$-IbuHW_+D~O zwiDXMID5&vbQicASVtII>H%-;CGRj>fIe7QZ)vdtbAe8vtGA3+1MUW|2X6#=VQB&P z^_DGUeGms|>x2G++rgdSHNbkH2lhsAFL(>M5Byx8-RzL-T%V21RLK$%8E8vHWNvF*#<8xEo#{TvaEH0jAI9Pg1f-$!M)%u;JyJe0#Cubl!EaA zw-e5AI$?3A$e8uu9`HtRZ^~{q*Xf0YrOL9oKzpjp?*i7q;s$R_m1mF_oTW)_OOrWs z!R=|X4K7$}VDW%Ag8RT(x-7-hWhoob1*}Pzwbuh1fnL~KzL*}spZ5gN++@2vv zzy(VUElOybRu6kU@uF#s;4K=iV~YL{_X1mn$huf2dXXt}=4Q$~c5oN429`$T@n*{LVOcT{&yp?C5t=G1owisfcvOyj`JKMuxwe@3bbXTCE#}Ok%V;T14|9i4SPL!BhU*A%RwAq zF3_GMqdCD{IUCtM&Khtx?DgP{KrbvU;B2Uj!-vYUR&X15F1URtW*cx9>~(}R|ATvh zEkos~T8Gg!Yr1WijAI8nhsn`%fxChAuz0||z!q5ea5+BK;n+LFF($<6DQx&UcE@x( zye^;{SU+4w_JDhlzXcXsF3RT0I8LAo7B{dS77utMxEI`)EB7z6qjozY*wJ$E8gMtb z2iSs~e1yzt9U)8EN60aBjzB4JH_!uY7_pnByBd+x3riDOT&@;ae6XA&OIRh#lhJsd zjHb(zd2HbE#3Nm~c{08o78lSBtjm*oq#k8Guz1NrS92g6iI^kNT5#J)Ioi44PGAkt zjXd?>Uho!hA2=I@IHM2;JQwH$x?ry(q?U}r-hrhB+^3exm-hI4nWrXSMspLV84TP5 z-U#jmZvpp#v(c!1G-?OW1=?Y8g1f+Lz}?{W;Eg~&24%;{IJPlz1njUl$6&9G+09lv zU1YC!y5X&d-HSXe;Jg44=vQrM4hN4fkaL0)78fjy1v0)D+y}H>CrjC{lUEHpc+GVf zAMi$SFSrlL$D)?8GL94If~9(_tg8kVH!Stw9`HtRFXc&bwt)L!XN4HILc|1*FWk-6 zx#A1)90ZFKSWOm+4_*)Spi~3wjj(uOX#w|vvvD%M6_^VgIc_)G=W>GA0Nvwc?e*ZE zadPAv!M(7zfcwCi1FdzSwcs}JTyQ(M3+M*c!(Q)L$KIaqfu#|aMzVy@@WR515VJ_e zv;plv7qAgtFSxG=^FZ-BHe&`amign0G3OV{2zFRpWXW;Xz~Y95mB_uxOQhXaB1`4M zQUi3u(g^Ma_W@ZcN|mBiDN4a&C(CH33ziyK+~6KyBP>2}HXaeiqb#@`+&Ny(9xm`2 za5s28xMw`ZAKVMOZ~Sidg3|}i%XYI?CohvRZNwv8^m(N0QJEY8C(s2;4Y(V;5xlW% z9c!H7rBdlGFYG?zE|;%tBfHbZCSWg2KxAO<1lfK&xN8EgRYO53msyFF2bdBjf_@ zlVqG4aNi_3wtTXz(FU{=<~ZGxWgZXE3-kf)Q>5Jm^h}XCec*g5+BQ{=i5=VpbOSxG zd%=A`UV;2TJJ3}j?_=HI9^w=koKHhepdIK2dZx)dUZ8Ed%wq?-fxhX|%V$V*&5&L< z&|yA zy+Gg1h`d1B?F(ct+&~X3ULd~(k%4xg>lV}k^uXc+=eOc4yH#FmY`4l%E^rsP8|Wj; zA(s!FFGOUZ8|VRgfj%I=4RLNmjX)19zS|H7oZpT(K>O`7rx)C(+V7BFJJ1Dm-yz%S z0eXQx!Wm9}C*lBoK)whTpd08}B+Gii`C@n%%Np&3bS{8^s4q6l&wKo za5uOI=!1p35y6c<0$s3pfL_(R3~_+&WiqD^+*K>>ZlDL~1KR4O-39ak`Eo=6dVpRa zUjaMN1#|;FD`YMFUc?9bfcE>O#RK#LeL%ia+HEW4JYZXiIUE)j&<*qgeL%hn`GIzz z3+P@Y>-DUX$DRikFIi?d?W<*e7tjs#0DVB)8tJtIT|oC5c}L*^ucY75{)y>$cb>>c z@tgP>{u2L$yF|G{tMn>smCecph3Wit-E>8|sk-ZRx9V2t9@jPKcIx))4(pnAt-2p{ zTpys1)FDcqu+==5j{O-wsn^^**4oY-9#gP7))tMVIQ?d%P@6Q&rPJ}`$HtA@MCOr$J6`aCZFV=(gX6dXC8^RJ< zF6+xiup~ByRnf{0)wH6+CR)XT(YlJS(JB)o(i#apxrYb?bKWMrKl3=DHKUm@txD#( znj-UTK%NisrR77Lv`n2YOWm3&OHH&(`v>{b{-I6UuMCm)^64^Kc&3c@J8C>SMCN%o zTITr{d3Hw2JOX*1E_TE^$Dt#2VDTANbmkP|Ep_IBB1rDS24DK;o{HY(h2vHN-rcB_lNENc$>kk9su86|3}=_W$&VBJ|y!X7M!Fp5^!pq0E>9APv|AdI1-l-4+-W0Y2% zqa#$%k=u{3Hyc3Mht}Z`)%LWoY&IZ%bzJXBC+8>$3Hxddup0kN>CKT*RHk9~N zgtU$x8&2FqC}?dEJMk@qg4PGgBfgbT&>A75h(AjxXq}MJ#J3R&S}UZ0`16FcHYFQN z`~^b6UZj;#ID3guu$`=k_%1@h{y_&0XS)dn+r!2af0>Zh_hb`@?dP9(q)=;ArRyaFIC|DDn$@F{TgtU$u zn@RjFLP0ByTu=OMLc#vc<`6$hDA+q}F7ac8f*of!5^!7kFOFPwctDA<2!Jr&NrB^2xuTTT2j zpXUd>rdbd>l(A?qL0i zJ6H`vm7*<#{T z*izzC*xke{*b3qm>^|bt*~7%Av-QMhut$l{V2=}bu_uYUSUvGd_B8QI)V< z>%`8-xNEoh5q9Zqg_yX3A z_(B#>d?D*a{C1W`{C1W>d@-9ud@-9sd35rnyV(Q8m$9|Pm$8S5*Ru7*>GuqYuVBv*U%{RuelOcj{9d+$_`@tvpGX+2 zPokqTlsJ7pM0`DqAikb;BmM~MPW%xTMf_0~P5e=2CH@$TCH@$TC;m9=Mf`Erhxi88 zm-q&jO#BJfpZF6jh4@C6Mtmd7AYRW(iPy6-;vQB`+`}djf0|7p{xqu~zJ*OEzJ)o7 zH?T_L4Xm2@RyK?HRyLdXGwcT9&#-yKpJnrjKg(_={v5l7_;YL_@onsO;@jAr#J97> z#JAJCgh}jqb~o|oSqe6+KbfK*SdV&B`#uN#N81mA}n3|b}j8X zrR(9YC%P(;{*kvvE{WV58PR=c_Z8hA=-#zQOpkdz7WOzE^;uL>&owDev1PjpfA zgVBXC-^TnDlWMK7erf&II@b0tTUc!GSa0kHvBtP1aXaD;#oZNuApY(6OYuA*BB6J} zgoNn{_az)oIFZn;S8^|Vui3qx>GgWAQ@x6MpX~i{?{9h=`*iCws?TkGw)WZ6=TM*3 ziH{}v_wCYmZr>$+pYFT6@9TX}^bJkwo>ZPRH)%`KD@pGreVX)RQjg?>oWZs)lln)sDX|ITsP{&t(h$OIwE18lm#9bNlAf-P~Vo6KJJW`PWC?0H4j2 zo6dEl!JQNUsqZ^@UOPil)u|Hjr+T8jN|XNXG5f| zbDRBhx{UdIzMZswuKerVkHfScvRuPYPf7jD%!xo9oyUGejv4DM{<)5^Gh~TB+l>FE zj$eAJkvrGx`=f9Es*~{d*uUo+g5%)NZ61#C`*XeTuj6;U-r@YQGd_(y0#Kw=d!(VF8sMIU2%;6xsK;huc8J8j?MYvzl>jVlyr99 zA6;^aNB-T`BTL%;e7-5L(SqN9{FUVd{FlD{ihA3R0hxJP&g}nk?w9?S^Vcs`=Kphl zKg4;|xo`A5{9p2YVUuO$pWIL9X_N-iQ`8_zgDDN6XQ@njlFFi#O(}=cP)frn4X2b# z$xdkmr94U_DUG6(PiZuzF_a1@T}Npwr9zrz#?dpHg9X#GNeC;ZR6?ng(s+7SE2A`l zp4Q4KO{6r5p4leT6WbIPLC+vv={~M2J%4ngd$?|N@79eu>50xosgj=Os_1F1n$k>4 zvnX9pX*NCE&7mi|8|b-iE~R;tZltHX`IK&=bTg#|lx|^GdZw__eVLW6_cppGv(d9e zEWIBVN6!&)bYB)n_ha#NAC^GR56SFqO7~Ezq2#6~!)2^L-DeG8b(EG2XRMC_O=GBc)9&jh-gc z=vgw2?nu(;{wa;_o6_ihDV^?;>3*n~?pMas{m(4A@412QcUIDU&Puw! zd4TR~p2Ysvu_I&TKG(6mW8`tCV~-ce<3-2rD$rqjQ-M5|b?j4|#|HLWfy{4U8anmdQICaHAf7+lhIY}s zg#7;OdgS+K{gB_EEgd7<<i*^V%iofn^}c&iY-t+V|B#8QH=Z-+8T75>B(g?n?T7$=|)`;uc7Uel(tj)C#3_F zj_G>xv$Xw|(yx?^`pqniQVgXeeGnf^+X?zaepKI&b3+in*|3={rF5U+4qAiJ%@-Iq z@QcRH>__5rO&9nw(`L4Y(&Hv0@z2<+#NVX!E~QT?ePy~#OCV8vN;&3he6m>)bIoRP zKW#Tq+Dd7cIY=BccM)ePeQS;py)8D8YuU^SEeXO!+Z!#4xW|$t9HXekft14i@PPLHOWP&lZa=pe=zmZI z1sV1@B*dr zN>Jbg#TfF9Fow8AA4-El#w*KAF=DPMNtA~44$ z3$*NHKm1SXycr2_RFAX`bWQAW)94(9vu=JDKV49uTRhjLi|+F-k&<(BL=W1==o}GlQ68a)`Ly*$d?QZN zwyLXJEa+OLJ4NYtN~UgZ;pldU?sP;yemY_<^4vtWm%Am2w`m&@N$)>Xs-?6sGKuz$ zTYMJzlJ3Utdv%XddbvCGzX$c7(p{7ur}Q2rA0=}Xt-?uZDy2D5ivFRfFnuZY+rMXw z7~FHN$nDuvKfb41Ori87rOarzxRcVO(LMD~(RO>ZBKAdB>0)B4bi*i3h@n+0DS0U| z>uP|HZ8p|2+ZZSXplzta&--~ziU*cQ!0SRtyO^6Zc2^V-y z!Zv+Tf?I5$bc)jV3I7jsZyzJ)b>54e*(Ev4CAs7d&B(O4(lRn5GV(6JEzy<~FUcjj z7MG;BOMS#rSHs;QIp*%nYG#)oq1-z&OSNWe_gYAV*u6Gj0~U}V4z7gQKwTtgjBB+K z8Ub+&;>K--LVxJq7>#Zn;2O69;b>v5q29~*Yd+ZS`BxAA%Qf%e?>{_vp|`MmPhoKP{XM_A`)bb-d_RTn zx!o6f>qvhY>96nJzGe=8U*FyC`A7Wq?|E_Ep*`)Mi9IXpI(xpgj=2BIZ;li`*Z*(v z_k(pCA8PkJfWOZ?ba&mIhyHHe_a6EW>wff5Z_m#k8Y#TmzoGcKPu*MmI{v=@sfUX1 z;qTw!@1c>0ibwEwbYy7l?~ZIJegl91J^ucWk^6hx=+&No|FNO9>qnn2j*X5K?mV<@ z%~PZ8!c!=}^U%=RnbCUj@9_73kM1daV(&dT_p8O9;_nyu>)rR& z;sN~qXZZUU``SHw_XDT<-zxq-zTe;fuZxcy_-XNXN7_A?4!l?VuMZ5Z{XX6w^xxv| zUmf_jm~%t%zc@Hjc>Unt7k_^6YR^9&+)(`8LmP_!BmVyJ(DmN`<<~Y8A2__Bxc~63 zp3fa__xz`aAL{$&;k9etIJ^OW?VkS$fBz1D?$hIa|JA4W6#fL?|0n+X9v<)ejfb!I zK927*c)tI?e0W3gFYxzo@b}y!nDdc`ivRMFSNi@M=`BaD7k=}|ANQR&vZ463BX9K$ zeCDmbPk!dTzBfKoEc|!){>5khb@74E{f5qQF z(%yUY@gp-c`)1&yJbJ!zx!PQ+G@hz7E;b*(XvJ)B;Gnb^zp+?vES<0LQ(E|PHaVC%p9*a7w5}2kIk2xO<#(}`yYv0`y59(5|?YHx=_AcdHgUs0mg;76V>^U zYbNFOxp@F@+2i%=wfTB^Zn8XkrCO_8Xv{x8rvsb^fIj*7!!tA28|6igt5`UYj|C~# zg)uo*sm&d)T&mWpOVxU9x_%lHoo&oj8mFrl8|B7Lj?Z>F=E3|iS*&Q228K2%&Y*4o zz8T;6fTTWjer$lYR0z;{vRsvYk^VXR%<(g`CX?>p&*_iV7Z%Hn%B131HeV~U{Z=VG zkgP#cCo4mdJrC>HldCXD0FoVUU z!R48m@rCNrSZ(fnWofxldnp+MLxVUbYV+V*VfLw|My0$k*1TDp};|L>xzcG+9*Z%$af)mBc-qOHG zfSK%0K`Q#jkarwS8*8R{T|}C15LHVb6JWl2-^>B_?-t`ytiwjhgZtgI?+I~%)JGI7 z$!tgj!GcWA%)Tvz0#f-h#5V!IBbAX0tjmxPRgU8J5EC9-lJ*Es3W3?~{#poP{dCk_ z(%G4)mUJdbq}U>7;QiP!Z7zwhjomP4%SWJ*GOUdJD#VSNeBwrhp9-nRmpSI=>la_x z?@lb&X7M#axR)EV6{OEB&(D`H&NDeNURz$MG%P3Vl`ayMR+&o`o@-R*sI98vZuUPJU1sbz9e=}7$FC1ak_r_vJNm&Yc7>BFGrgge{s1yUtPka zqRCj6j4^h-T!rN$%@i+?ZOGw%+m)%x63Y!{pFIt01I3e-g?i&A66MBaG+L@Ga2Jd( zvsw#M@9736no>->#I~SBFyDcE?m@99YBoru2#Ygp0kZauWh`yVWDeEP{$XPS(|3gU zKq`lcrPzxPv`BcAb`E=J)xiU9qPB$3X+Iu>3Q`|YsQ5n8B@cvTnnN5^k%B~m6Bc%% z#JBu3yZDxpW_NIRDAmjiWi1Npjq2rUtvv4)3<#_|S6(Xn+66U%GNmB2#!_X@Mwo|Q z6D?{bmuT%w)aEKTl17)y3l*Wz+N@Z#m>Kl5Xa+8_4Y9PNg8H(UO*YH&(qgKhxxBCd z12pO|U!OmTX?zC)aVBH|Gprw7b91S__;h(`_KMFFl5^!H%Pxi_2T`4zbftp!8L&l^ zD>r;G%D}rmgH-_T<5kO-8?e%^!5CCf%1fLa4u)nuzAf9ldb3eofY4cj#PLn8fw8ee zl{s>yFEe)lXnY)!mS5O)eC03%bUP|cmm%qi1*wGCwN1Abnyc2(k)rs33gH7@&@3-P z^ny!HHtNfZ)!JpGy?{jF>3aR@@*;|!sN58}bgo=&u=Vk(h)wJ(azWUpiWBLSDuql! ze;||KcxGo(Mb1Giv)feIkK%4dl2TTy$XT8a0Drxu_Lec3|rjCE{rvjboIiG}N!wnRmZX6=*WJ8N<2?6UA6dXe$?1!UpN z_A{4bd_A_F9l$&Mx->I0R;$-;F4UKswA(L($M?OEt(m^LSlN$KF?bHT)8*f}c?_$o zpa>km)}AVv6r$XJD3@|r%m$=uwn#QS&K>fG726uM`+iVf?AA?^Qmmsf`JpP248W1J z%fUnL>GH*k z>2CJ$ErG7kR5Os-6vf62sD{jsa&w*{Ph*3GpwYs!OA<{n7C_ZvqkbdNYRES$q<+z+ zY-K79HP;PMrn^vn5bQCg|CTipfq!xsPks#gT%deg(QN+_0Fu?x!YE4blE?1y< zG(+O$76)*G6c?j|zd5_A=sYK8zDiAp7Hl%-Y{ zx+F4+n~Z#uLQrD{TA+02rL++p8k*civZ{WB(G*IS#4ks}P@*Yd3}vK%!@8r)`5pC* z;RBqg-dJ9ouD`@e-Q!Q^VsSr<{T?G}1F=~99mww|#oCWaGk6eefngzrl7uMzFwlJ* zCfQnFyl;l3-COIcv9(C$ZLM!`Nc!ujlJ4GG-x9ltlr2;;y0xM72wPmLHx^8WAltlJ zU0l>{MsA*xB-vIJsF7*=i2|6gvsZ$G;|PfuK&D!|1l?AdgMYUK0V|>a#U3oAR*y<3Y5uXAA zkh1k72~o8SAIaOkk+IBBUjruvh(V6xu*4lh7-FfyRKm_~sg4lgn2`3A#7e1V zCXP!121lw1s@D|hpDXf(KFLy;2QW&t7fnSZ69yVRc6vgF!+Gdzy)k#;{An38%5n%h zuk^K1mbFh;E;bQ8w7yX`%yWGDY;*M3@iT7fCeCsfMuCEbat)%qRvDc*Yo$nqE;MVJ ztUQ6hZEfkq^tq_=sS{HI_nD~^PYcA7fKjzvnVE^&naHR#nxjV%udOua@X)_mXscv{`l+@yF;CJzip5Ch{jgk_Y)OAyitKOKJtjlAEYi<$EEx8VsEzEp0}N)8X~8 z&Bc1HDYMOSHtZ*ZL`j1MALr9;M@Dy@iLj8VN&{znqd4kyV{*pBlZz&aKT*s9bd4D z!gD9;jd6I}+MkU8lV(n?c8rIu;KG#MleTeovVN^HUDv8(^~IZ(_TwMDxm0OttpX1; zgd)uA>k!lrDFS-K3j9FRl^fQAae06qN**<_v+waamt^+ums~H!ZDEES0~=O*Xe*~LnY3w2%=OkZgrN1D`> z5Sa9_<)}>Z&bgZ`*KlN#)J>LeBbHEI{0CHu{2hCJ6Lgvk&EMoU_h5$eYg z91q3LS6*C3(14S`jj7FnJQo=G*0zS|DezyY;kfxV-1uo~vhEH!#X>x~CEe})UZpFl zdUD~;RIZ;~u7V#TG5vUzE^TEVrHOcs$|mNb42d(!O5`SVzl7RM(VRvzGo?!Kw41ZM}eMkUDt(#4ebxP=up^lS@T;_<69Ge;3xgk12u7|f3Dx?ps4*U>cS zE~S3$E;Talu2G@UU2amNyGF%BcX=cs@+-X<$f>l5dCY#@&AM5uyGLu%dcm|6viqJ z$5*Q_&*5CMYh2;!uF@$vsq8K$yLFWsX$fz3)h2t^-7(s#?#gpGb;KDQOiUOm;Fazc zuqPQYnP^T{;d11)5OtMXBLRpmSi#L#y62ItywIq2*POj{sX7azt-D5#e!3=r8${=* zyS*93t2Q95tXk>AjJYEpuQX>H)kUhn?rEXwW-Hvk?yBkP0@P)BVX?bvY#H}M8r|jC zkY;zOpf9KV%rZ=v##tN(cw2@1mD3JeMG>_O$Fh5H8^nw3oED_YoZ{&+LXKVg1B?A? zPBG7T$jI2e?mphF$*DP3Uxu>in&iS#mG@#K+SgU-HHY77UfP+c8HO59RV&xK$0KQ+ zT87to0_v(ZH-EFMiCpCoFF>3Xr1a*ocUHYcgrhB7mpD_ovCOM}?$pxKBFGXqfgC(F zekGcDG;+7RVskTd>S?=0*pcwX@D7*?bl_~oO*DBA>TKibE4+$B876ijWX$Z<=?1eW zD@*k3U=OC8wY!T|(T+;3MGJ>5xmu5xktvZM?Q zA$=KC)QV=kpXoj)mpVA5_gv(kRL zd=)fPyN26o@vS*rPYYM$cuRvGHLugT3Gq3j0-}^GJAR`wyS$`rfXEv+C3(EQ%wF&s zmmuP(|4t!bgAx!3bF>V7$CoyJ$gtME^(AYyi-a+NAewb&WMT;A2UZ}zmg0oE6J!OE@Pe;mI=3{>?>)IQw&)J3k|;aWn415d>)ZYt zGM*$jAFV1Wk%W~5i!7;&icVIRFoSeRP#b768HlJ!hEHX@bVnJ%)h5!o~Sek+wVq51As&boAzl z<=^=o--)QK@q_DOz{I20C+SAe`jG0=<>rzI9x1^{=>i{V@yx6}pr-?ICDB-~3tVi^f!>PoqRm0hXe za}u|PA&QVXR$jz)f1M2%jOjv8T&~p{m18(uktvR0ih0R_R^#}r=?-^n9-&KW7ltt1 zh1$7t1K$WoX~LvOblmL&|88=59&uIdpj{@SO@L>a>|%!iAZQaTXljDCuqMZBY+sYk z9;d#=&Eunu`gPF++NwqaMw5>|zJF$>$(;Zw@}+k!9KxNA>x@7O1m4Fs$<&Y-G2>BV zOK?3dE~A;wMg0-V>~?o3>4GR!8QX{i3&nhL1?0Dg$5@0pLX9XjS%xREP&Li7_oWjJ z>>PvMp~B`rRh>f|ER;jxRBCiQLXp8k((%S-cjZLwl0?bSvpm0QD@fr>)yvC>;H73b zU%51exZToCiri)xe45uX$LA{xh}2P{L{+L9e7J7Dd_z)Ey#Wo3%`ML^g)wa_s1O)p zPWH`uaO86BW~lHIL&}$`7sVnEwbY~+n&2r!4^?851Q0-6rtPTTWLIHJ>JNf*3X%*@ z^N~}9te|T(16BdMYWiAI?S(t%hN0S!TcU*uM^WyQ#X-2k<0MZpLa9`a0V zwPimJF8}fl{I!akOHzJpWkPsK%7aXBX$h}cO{#z`vH?HomITBkmk#ntQKAXym`OA~52P*%7znVb{FVCkI4^5}v{{m|;9ltAij33b)L1^h#OXPE333&Gc*N>> z6^Y4m%dXZr<@Zlo-Q|POitHDNnphqPLW3~ z5FqtAtF+4%;*v?+MG%$^#t8Pi3a6GYHnq=G^~UM)MLg{_Dh3&K)8Z)Dgzgrh;pTu+ zQ{fvU-*h2cS{O%KB$>($p4wrsT*RHKYb6jzgu7!`=-XWbENYX9&lXK%ISJ4%dvfXHkCeAjkoB~BAFx!Szl>Tu$-^g zm!w2*bZI*ruh;pkt>hL7Er))(I!Cyd;6IfcbDCrMY1m1q7_ZGW>4^hvcvwS|tUg{@ zL}4Nb;%I_IAI27|ZbsBNQ@QPu!ejG@c`UIVM;2;Bs7`hz$(*FsXxG(Pi-O=|vmhDH zUAH52eH`j^efFwrA_@i-Ch3_fAAj_uEF~2CAj6~cY)azQ)VWIKDy%|$OxLIDvy3bW zh`c8*3924B&XN+*4w3?W9)oEDyJ?<>y|Wb(qx7(FqpU8K5fQ3w{IfLL#xi#ECNY9> z?9JifvCiv_3>rUnalt?N6~vGpOe!>Z&}N|yE#_k|ke^kt0~@0t_R4&)yzTij9PwTJ zB9u?28U$6t&ZSDD15;qBiI@VQoX4Q#-WtBS$EPdTkOG!80mFL|2lxxi3!1OMRh@UH ztPo3b=Z@FqOV95emPLP?MI25DL9i84UXS=&D*%ypu@$C$fzdgP^F$RNv_TMHS(wjw z3!~Jtd?2t%3~?kif=92gEkS@776ABdfX8V&A7V3TxhT7Uhza}?Hs(O!&e8&D9q{OU>pqXv@*NL};=mlSo9-@!E{0Xoz%09O?0C z^%9?>l*5164b>WX?0B646B+E7KC9_V!~qnsa8`Iz76DpDW6r0k&)0q5gaaMLXvhJ} z#5t{JRzOgPI-<*)05Z6xS)Ms%kos&H#s-;MFXSM(mT!cxh1GC80ukrwu3DZ*i}M&M z&a%6=o2|z+JsdF!GI5rdsq8+PM1Uc0#U+rvOfm~f!AX7wlGr!HB;LG+Yoh2-Yn$0b z=#R=JF07i;J8F;Jh&&)lgNqtDS62siKaK^2ra?yBh~wS~vS5>$Y6^`PAeBbC*lfqj zjiph#gQI4vYA;YJ>?#pi1e)6gk?Wsm&9{A^Utx4sYO^>ypyXvgGZHJ8{A0m(c`ICw zauYrX9~E)U;oTeFR*DMDRVF1w#b@QbDJqz&T!N4mNJton&1+`j?D&mYm;(e6F<)*D z!awCuSe~1kxljX(B0@VCigDFOH$nL_t`g3uFP*^QOVokU@X-B2tt@9)Fo);H@HXb@^dmFG0hDNh(I-8=~-gh89B$?0!}I+#2m1G^_=$af)!yFm$# zJo4>oP*P4ncm*gaNT5!Yk|%gC$eNkEJyx5ONOTf{gbjDZ%d)`X2~$k zdEo4H74LRJkC{t2@{jXZfbgVp-nUXoA!lF>Mm6<(t$}b2$I!zCTC=CiH{n^Rn1O&g zfw&OrVJlBrFty=mbwpY6v&LB=Tc*b3;29f&=rimBy*>es7Tu|rh(vK(627fZs&z)0 zhPXOHopa-S3|<%oOOqTSy-U~ZJJ;flC}3^=YK1(eSKos2F>L;0Gl!|p}aUy)9peHv>toP}jlg}$MW z6~xV=qOkvwF=4?o@n<3s>E1-ysE5ga6lo_sA`&;Hl%)&Mq`ioYrtSKW=DKY;*(VbZ zO&@cJZnig_DT<~vmAmXR1>BlCf`;D6WoUQ)o5jQM$Z|B$aXFHG;AAkgn^<<%OPSV`)e zHj*CoL5KhWeqQwh1fxD_u%nGcuo$|O{Q%-La-tA>b4K}6^+Q}l5^uUEYS8W&sd}lZ zH&^^>Vfkb)@yS4THP57;n5(*ht5hxHQ3JbmtaC2-hZQszwl~FeJTD11Xqxn-wEfJY zUHZ{HL*Sx&Wj)5za-Bzsbp{LKkJrQTk$(HaN8dA#Umo3`q&i zXd#2O;55y!7cZR(e@Y?)mr9FfmkWR!7uoQKm@#7w42ENkcyGbfS9Av0M5Hz?c=Yi@ zGc!w9s=8tc4G0*T@RbgnwY!L-9YW>(81I^ zoSSHvL~ciA;4Ex!RG?;RRDfCi@n%#QpWH>oh~P!JF}`>Y#vjyJ1=kJ2as&lYfl_@` z;3bUrG@^r|=%g?|dmu;|uw1I1-y7^O!xW{cp773*V%diue zWo`+d>UF0;OT6T`lsl#mx{gW!%ZL~p;P0r3AFV9Ra|VAQT^KpeCr=fRo~q0vOxm=o zu9nTn9j*}6fyrUsH@%^Z=#pw;olnI5DV$=dq|=ZoPE{myYVoT$n&+88fSZCCr20Ua zT(iR!6p4ZyTLQ=CM9M}XA{d9$+ZXDOG##utIM^!OAo}n!%^Gq?@nCC`Vb}&6isIB$ zpyh{4o6UBfoE}i z9_VGU-270ls_FD}U1w7*t;iCEO`93dCd=cTzS65RpF4J?g3I$fc=83++U)%DTm?QO zFB8G+_2t0`;5j{P0UyJku8X&TLk)T<=jM57PINWyd1^u;sJxv;|FwZTRTRZISm7}+ zwuj=KUnik*;PlllLq^aFgGbrqag?N~^b#XikAd}W4OqZuMF;tGT@H#N#(748>EsBR zxQNV`$Y_te7lcS@-A$n>_d$t<0P}9K&q0KvnjoF3R4-py0&B__PFN;z6ptr)mUt$J zb!(cpfv%!V*c?wWPzW}7ffD8p0IK4ML7s4yA$^>Jz>42~wqYf>huK^T*=c|QLQcXb zKI^SypCcvvBnI>;EY7F623i%6Z!$TwJYKcq3m7cE&Va}@#QV!=ajpVAjBkA~UN20* z?Ul#meGVMSwP!skZcl}J1dVaNT+Ok$yZ@tDN1}!g*UH~U<z5#X6{0iHot93~(N!!SqAqhdD5hqTg7 znLSg#E**pkDrf3X;~8`m!7ae2zYsY(brW{S4NBX}4TpD?2;GWeoeJ*=1cF5w#YJjO z;Cj7|BuyIL-Tzi_ZbMxc4tj?J6-L(d!TxJ}Km2 zbhuJaWC%RnrDL_5UdJH{e^YK^Vko|MgA7e3!hMwm+_u9)!wY87<%53*L!jg#gb>jGc3c^9;+z+liqbyr4mtalS|> zdNxk;#Dc#(q1*JNz8zdNu76WF(-SpZvxdZ30_gs^!EwBt_@-NAEXTRl4{zb;+lKM; zaJb}$AAOt0kH%qH_*}rx#SOd1@OKM8*T$cA8^%wjRnYwqu4 zg4QtHlk$VU9FY$Ed|a62P*Pq8$2Rb@b$BcS&@4u64x`O5{=Veg1%>>03i2$(Cp^Sf zZjfH+g4B=|69A+R zWLp!PEC#?Pivh4H&H=egKoxF!MH;nFsC`hOG)KlkmQP-)X#sNh-3OdHi8O8;}=yfDjMoj?1|Un`fmp zJVn8P`|SUR74-h5a}Q5qZL@-Fj(G_XjDkdN2qd^1=U3iD=_!;g0UM(Z{d$M_gQC5o z&fUYcQq(O0cNYOw8LPR9pGh6Y{{Yr zByR87Ovwk2<$9N$+f1UgEuC|2b}l1Xe#SNJWCp{<7TGp=JYGv^9DhEab5 zP*gGZP1MBwoe=4Q)RIlzu49-f~_mRVxlH2q@yZ*5?1bu3Kh3A zh9~St*hkk}!!Hn1aZwz(V?i^KDZ}n^zld*+u0o|O{GgOZ9x|BS_Ty+nt{~fKt1+ZO z&)bb(tY!rDtSMa1_EDU$E+r3$QP@Hjhz<+zY zq3g!_2p~`^uFz`d13e6u?(P_tF$`%%R8p{zEeJ54WW{e4pGN6!q(>nHMv*oK;D+u+ ztrDzl(H@ZQ0)DnVI}VLVhwJS5quW-~TxAO-8!O!-qU@tW;6~Eu%wIR>;dZ!tX242> zi`&Y!6<}f!R`l;*K?92TCPevO{7?G5E@)7dxQvGILeYV$mTHUgShYU$3#(WPq*m@Q zEeWNR5#$t&T1K_av4~(=5{qcxKnZILy?X^{suo-mL2U$d4z;9daRl>lvFz7$g)ih0 zOS%VXH}o)GBX5ykXkw5X=xY$tSwKlxLDiG#ZRl#|^j{jC-_+gcZm-vHGT4r0q4Jlj>K+Dqw=Y=0S}#5%l?wtda)YK!`Gwvg~rzXQA3&oH-IPFtP zC`1rtG>KY?g$zRW2xGdRwp>qY)Fa zVKPardc~2#uXv4Yh45rnCq{a+9nLpbj4g@h^hl1y z4)9{cTip;_JB7*in7eQIAUuq(P!MYHAuAI|-E_CYdM6LyO18!pC;Qk;64)Xfy6 zh0EQzF$0{^#o7fcgM3IYwSoSzK34|0!;VZsuq60*W%0SI9Tjm$8LfF5-sMIHq+(Llv*q+zTY$j04s(i7gW zBZOpPF2!w9W~CCJbA{~`A-bdh)dYGlz(FQ3fw>wqHly3j(TPg+A>6@hOu64I?LwU+BnR)LdMpj|Y4H_0wpgFEvIfvE9E^pS1QlSmAwih9pt=ZPF8;0_B zIO%ov1Td*)?=1xN>k_M@8A*Kdo6KtFB==7pggE%Y;FY;P?ulP@Jkm!Tnu^AHEWIZT z&&KnT2I4&%@kVhKbam4e{#pJGriDE%wDhQ4IRy`E(&JkVDS_gSwQNNd%r$UFgTdnt z=kM!oV3cv%Rd#o?3U8%b?B4b?9oxR4=3c}32w;iL0SM?9PXiWGv(jz4*2E;~Ip41i zN+*5%6stZ{_A1=m7_n{KmOrXdJPmeIQN}rZ@htkAlcf%+#lCH)DLY7yY(=b_Sf>#~ zr%EME7G_n)VRlpOh2Be$Cn|#|UtlFanZ|ZpN4Jdg(C~XHl4n`xBW?%x<8aueASG4( z>2U*9o>F!QJrtfu@1IKJt606t0@AEWTvMg0S}y|1wEyDPj|$W<6E|PL2w@aAbwa3^ z+)o}>id6H;s5+=>lhwDJarl=IQsE)wIABpZkHf#MBUp`cg!L`llg^J~CSuWTehPih zfUoi5o(l9-9&{ZF@{tM)Kj%~C_D!nDLf#dQdBw*5vCYDlitMuS5(|{#h%Ehkv z1^0}c`rLw9q^1e`>_%)a-cdb)nUr2ZZja}zj}mCaJ&sQ^4s|ar_a#@DTy4%DBt9Jn|0>@@(Otm@We)8i{F&cs4<$Mh1q3J*{Da>5?U!zilWNVu^Iau0;@2w5W;!Dp`Xi^l(FAW;M#fjKJ-&FlTm*mjs6a(wBe*JEsog1KT^p z48&kC!V+(CEXV*;SLhND(dMIP}CAPG={0bOtI+N{n^)(6RAEF?cIHdmvCNYCPIKNMH73nP{bav zDr@zY3eRTeR_t&#VoVJaolSdC5OO6nPB_aj{4v8&3Wm*eC>T#4fyjor_wj(bEAqZaI2Y49Y=V3dcD)d5~Zm4*-1cxGr$CGpr@aEQN z=#O%TG(z>fj4_}92WigH#J(=D!+`%R1#n#QD~xu}0$?*$9Q^gdNh$;@SA#HCPOA0| z);w3`7#u37=OI)4W-7$vy5fEqll#yrP5&d9Ih=hIo@R80ZVqE*aqNjW(i4r~>vMO* z`biO!z*8KFF{92sp9iuBjVQ{?{VXRz~F>rTKpm$B0tL&L> zYgC(V%`tfqLCw_kjHD-x)^oL4dL*usSoQ|ulJkRhuAfiR+|#`EYfDN6&Y;>*M<9Rc zSro_D2uT_1VJw7^lORjD9_qWgU#;7m9>G;k2<5kSZh^ zCfy91UYpxs#16IA`ok&Nty$SST}r4n?oAIsaMTgzQmARkwFFA}lp1@tP=RR~E=Ly; z?+4MN3pL6ybC$=#G_!;8zQ+~rk7LDpee>+zzs+c1~w z#mTHlhtS?VeHsf$%M;~$G8FYUNjnXRu_^0pq_j;665r0PqpN18*Q2u4RoLTNqf%M} zGD+P%#)N7~B;`BPo&31$oQJml29R0M^Ir(_qjwQ^RvhuRWB^{61D zl;$4jJ&k$HBHTdZbys2&Sc!A&7E!& zTNw1Ati4Gr6V&OIxvkfPFBO-h;u|>en-EH5I>;@`9I8*s7aJ=b7^A!k2m3J$8Wc2$ z-2L%-OyzJjI=?%7Or)jhIaNBup^1H_xMw`Op>9M?q4Dg;K| zT6#8a6X)i>P%)`ARL;Qf(4fYweYc0rxpa`=^sP7 z)XNG9x%L{a>}p%P!caiak5|eVc2gStf-9_j9`=Ikg=upSYqNJn(W0oh)cW)2$-O2A z5qGE`%#K>v#*%Ey!h@uD`pDc9TGDBnp{(HluJzB)V!7$FhCH*`ki^i4M&i0n7m^M) zRP8`+sGgY+gfxTh6|mItqlrv6RAY>8@`T?&U9u`h6Vp1wM~9XtFG>#iqtVbZx0eY1 zpq0CAqd=k(Sg?*<>rc87n8tN?#3JSbB8FB2r#IB<87%pO^gri4^{YSBMzbQc-(l;Dd+>>Gkd57eq&}*#Y~l zCZ>r`71y!rKL!VV0yTOA%(eb^4#j~Y;r!HM6_GdRV21`2)(rU%Eq0Ny@@ z^aO&2Cs5*Af0Tz>^&=7i((cH|C4 zDLCWoM;(XJR~})KNOUYd>qmJJmw)u}rZA3L$oM^TflbGwe-+AIWkRqL)@8dxve0nJ zZty7sA^HHqFxGfDfoqS)0!PV?VIzR3c1>VymoPJF!Dh&MdW^c8rK@bJJt!L(XHu%^ z>NRmzu+}ZZvQm^I=+1L@>@pD}^jsScyLBeJ4@;(VPaGF~S(!m`O9D{3SKK9rqco?G zlU1o^Z+Ut2C8}HmmeOj~MoX)3Pz_8;!l>fr7U)rPN}=gcq6#5XzBVG+T2J$^t{Lha zsyEh#2S`-7k)PtL8`MGMsaxVoHUg#jxmi(&Hd^PiEFkxPgmjRl}smQIF2>ZQfmW<@(sosw00P&tTV>t70>2 z^%BoIrp>tO*k`A(8uO)smR2ge1)&`1nbb&Zsc4UR-Q*MKYitU1nYPJqC07SH!fwpB zx?KY=@Zr^H`4(2CxVF8CM|KZn`>AS6wb`GC$!_e0nOy6Y7zKHcz63r0!Q{RgG1_8c z-;w4XO**=&B=Yc}4M_Ll4<=pqvWRwwGGlA@T0cWfn)%a&O9hifsIf}?xJU?DWQ9rB zcc}7zelW0+~L=5&8z5#5IN72WTk7aA?UF?w=IAleo=T zjaTY6pF}-%3D1)dv6@Q9%*YH)V8*0zFn@_LJG-@zL`0Y+Mq0W;Ao zL&t=5X#-fj>M!a`whQWNABQdJ(tYz1mtYt)GxL%)!*A+Crt$99MfrlkzY z*iM8$n%NWDo2jEw7@Boi9bkT3T9$@6p$&e^q`D!A2%tle?X!wj*kk>T4AWX6oE^IH z4>xeaHXAe#Vk^&h41LKxh&y|)O#`D;aHJVVEJ)eb-24Gmf81~0KB(fGOUj~-J(XJ7 zo}570Wl`xY3P@LB{0GsJ8`k}VP?KP8@3xp|Q9&wh4pd8<-}S!a+(_DTCY5!EO-JSN zY1|MwYgaQ(TF{>{*EuWKNq-+Q-lT3>I$nB7a7xKS+y*~b<&n~Sunr9(u5Tag7L5nf$LGB#65R*Wuk@>z0j!0{pmT9;^8q4zt;tdg}k7s2v@u0D$ z{N9u`Po-u74jwxpYgR8wPg=sR=QB!}#~qGsYK?bk;Gg4FppQ*@rEJsf2Dy5YmIE|+ z{yEqJSu-n|cUP8njH^;w{a|d97$lU~>9B07PuG*bhJzCxyR-uWZ-q7ALV!ngB-XKi zb@)g{CI{$8dY=k!<1pH*fh!q-3F=V8R2o*H0x*F=kWgw?hNLgv${Rm-5|DSyg;8x?Ytv=vn*8s%7Bpni=TsR6GVouh|{2rZ8+{J3cJK||Yx z%kH7+F7mm7G*}@35zGa)(Q88#A>nmZymQ_SATOn82(BB{H?vf#v>q^_YJL{<#F!Ez z=sKlE7>GO8@>z28GPn?y2WxHE{Wb!h=Kvn#BF6!=wl&P>k77`;&$!G_C@kt|F)r}h z{Xw%*hRz5?0baL|zsfv%3>0Fn?yt6X#7YMZZqwlTkCX)LP)kw%XSXN27s{uKNjH-x z**@x+AG~tW^oO%Jqt8X{K>L$MmqMr3Hu;UZ%RScJEA|3oxmf`bXHM~phoFo1<7|W9 z2XSP?m~nhBUwsC|6Sq=J;Ky=6XL`O4YxN#$Nx2tXYj-cT>k_z@J+6W}2&q1r<2H@M zU^@%{_8g8P5Fw@BhMnI_ebVdQIh+AX}Y&uK76Nq*xA7B)$yG z6l+j(*&}w>`q8vl*sDGMXIyP^Azx-G%DQC+Wf;d-c}jj+23ko)^bRPV$y*wQrk&p{ z4ahqx0XVbTj)e0D|3AH}(E84kyN0np zzxw}^IE6a{`*946PQpJ6KkN+BJZ?M=nZ{c|OZ-N|1`#!cMjMr!- znQmdgT1D?L_>UsW;z_Z0LYdGV8mF1399=_7kKL^#E6!stI6R~cc_ERa51&gpKC9XB z^BGtPVSohK!@vj4Op4V8N{FZ6Rh>ag$~d$fGCJ2-n~t6MK&|!|w2TV3Zsgl`TGYox znLWfoHTqGIpa!dMG&M;IDLeIYE8TJ~Xa>tkpLMvCXHa4{>eIE{tPdujpjX|Cfk5o} z_m2(h6B4|`pz{;D`0*%eunPXCk)aNasvI?bXo#yh$vWaI1ViAYZBR`}_2F7CPo%12 zD#=qCbSehhxa{r?;__Elwu<)PC-9!MYpr~AFmyf3pb+jn53^|(k%`PW)2N~` zmWasT$tO1X zi3V~`?7P~bvxe^}4&2`K3JH@6@kv0O6F3>sBMuFjWSyw2ty}nPFkP}7@}V<5o$dQk zu2uw&UoCXw{JQ|u!VWTbg%5@m_5Mj4rY7D zM*~Wc7C24%JUIn5`#}ax`uu~t7C&04dQ!Ej!tB=Dt!KA^`=Ad5yUkxn%*f|42J>ft z&l4~#CBK%T9k*WJlYj%x3BDc2dNVA_2-XJ3yd=oPKE*$ z+V0jnG3OJQn;wHfVro;m+r`*v52dX( z-7*V>F_6C~yDQy#wYkQzV__vWQ`T#X4~(Dlo`eaw&h7Jjv3D$&z(0T z`gPsWF@YOmqI?4ccOuN^wlVlgzT}IcMgcQ1&bQ3idw`qZL7u~gI$Gm&3#bS+B$(8- z{v=J7WExJXky8f4o zFQcm6qgLAuXwbO6DO%J$@lOY{8-E(8w8J!#IM>eTuC3ss;KR+FLqGmH2brEBO;s>i z3*3m?4oZm&Q&{ZJ2kg=z6osPj0Y|)HEZ22VVQa9Ei{@3-S{)ysU>u!Slh|%G-{BV4 zrN^^{uA>bb>m%W+>xKJ@m8|-_E()8pLr;gcBLx*P*3D|Sy2>E7cg9o_AdK*Z*M5g5 z8r)ue&gm(cC;h@4eW)-fbFuZ;%0nlcCxEkH_3|BR@pE);kfOr^#$2h}5N2>s#MH+l zMY1u2y9TeC4OXgNC~abf$Ec?H8WD*EZQs4`SkDf=$jiDja==0`1-8=MHq<;+i+vJ z$i4Urrlq_Yf7@QErF%g}^N&=G#4$Ibi58SO-y~cTJr_uE{k)K{dr*n}w9JZM%)?6baUgDFbgSiFk_4Z@ zB}s5g<2v13zTw<%-HZY!;*4&30Pt!Q<}uu*q;R5Drcl}{CQ%ly&GL~ZVxBkOh&kG) zWWMYQ{0)swbj?Y4Z10eZuJ+&5AEH57eXou;?GD21KKns! z6;`f}d%>M1c9kX3_XBUym_LDbCj(om>-xc=7PGIO!hGST60EG9u@gHXbVGp^?Hr!# zrzkTKqi|<25Fj(ErG-PB^S3J4ha%3Ppxs5EKvifMzku@;wFA>F2$y1rP!wQ6=e5d; zuGL0Mi@d@U3@(tK;yFkI;ZBfQ=O1Qh6nf!gZ&&D)ip>(@2!+jbY-!!h2sYh4}2TmFc~y93Li_gUmzl>&3Fd6}{`pa8pD}@c8koiSm#suKqInhk zJ+YH`{M)3FUEZ?OH*Fp6gK;19E)4NX4W?!ZwJ>?tt+#pHVLd6K^wL8NwxJ;$jALph zyGPCx`fuQy*?6}PY52R{MxiUtfQxb4t`~nPGe6(R<_=mwRewcr0yEGepu`~MyOG!` zSZx&Jx7F`G<_DW0>DKajPq%3jcuoR~>zSj?+a}~NKIyA>H%J#j zcJNRoSZXF4bxfhN-shYv$NQ#sQ`Q>FO2XA2HcB77_}k%lT7z%E3#VD{6`soH^*6oia8^24Yj;b$jiC=m*P>L~4B2 z>=C2DI1C@O93#iXMjTCa2d%F{L%0G=L0VBdCiDHzw?9)~^Tj_q`Rdkx^OyI%{Ktd* zMajaN;Xm4Yqj0_Cz=-*b_*sCCKBv6*=D_ZHc(v~&0 zrU5ny+5TcugQb5gHL)x)Q?#x0@uCey^lV)RWUTub2hCRaMXlRX%=GBlx~}L7rIjP4 zXBGFatSuG>TmRd-ZG){J6#9rhCV;kmg+gJ=eQs?(tNVt5)+&kzi|b0urJJQMm0E56 zw~oU-HXx&|JjNaF0qRjUF|cM`X=QTL@R|a!H7Wll`Z>_bli2ijpC#6zIZ)611y+z6 zwmOdcV(G>8u7_WPE6?|7s(AC= z_#GeLvZVl^T5l!AYx)bN8^tZ4zh_I&GV>CaT5A1RK7UU>UzgACv+@~!e2yQ-rSQ+? z^DkMjB+0MI=U3$O%PhXak9mG9^5apd`3okW)4mHQXX7c=))$K$|zVr+*=qJ7%Ht) zWxjnf(Y|$C3T{Yf#VQU-#UW|L6%4i?>6dN$Dah#^Liy7|A6FoGkZb#8pmr;1MeF*l zPKG!oL!6TIIZ2y=g}GAu2w`cxT^t7OzCE-90|D`GV|PGQlwE8!*sefi_5gXxe3iXL zP<67r_IsuFFLZU1M1TDAG=3M`FtBkQ?V30o1;#;;B zHfoU$3e18)2L{&k<6}_Z!6*s&0xD&jzZIzdRv`Sjq<;=9%Y=?Q29<*hz?ByNAT9oZ z>5gL>pG>cj^cqR`Nm|z2`M9J%F6A46r%oI6aaU9*l3R6%jiR2~C^F~ogbE3BI-J)rShE0KLShShy?CXT@7}oiB|0P zT2PbeUhvgjrCLe{kW4Hj?klXr-f>;Jd*2GX#{sceBC92Xw|mFzE0f$%=B9_6rRy z&?3ki!UuW}(vF6LD!I<{#VxQVkf(rbzkz>i(N?o7ZNi`(T2+vVK^^ zN8ivu|Gu6c+L&AJD{P<)BZ#y`VgCWrd$yHsuU)5t5}zQJcf{V?j`HJy*=n`XG};5rlZgS`Fo^hPDAIK(@X3hjQFP(j zBzZGku^{$%QkQF^!K~KRb z5F3K#7^=XJr8W&v_H0v@mfA8(d!wYcNCU{j?R~H?LA32{((z}Y7NobJLgxJU^7(&x z!QEaDfdkK>4}9Go>hJGgM`qp9FGEr=m)bi%QS1lZ|D@C&F12?Kws-9qZ0{LtkB}Dz z9WeP5BKP*5!P_H)xA(6H=dYbsww{EU>+kq~7`o zlw_&>9VCdh_AYQNK4Gg%c0@k+NFhM@BsJpgF$$#OTHs#jpN0y=Pk;a5?Q!LUpHS4p zWadJ*x_)IIZk%{O12lSHE({p|K;Q0eeYyBxU+Iu3w|7fz00kecy-C{Q8f)ZTH6+$P@gzU#-7)IBj`ge?Ef+AU}jI2qFOxQ{*Z{Qz$uDpT4VH${q(%uHN z0}%rWE|H)RY`t3oDFmH{vilsxrU)c}b6do5yI`~^?}~npNcl{Q!sjl zz?-+9r%8AhT9q2{?S3cHio&1>5!{{_=&{k)DiKoKm%tPJeA0v5e$pDP?-AY|fQtbd z#V8~qqtH)Dr)?u-mPf#SWxosj23Fe@|6LtyS0xF7|C(6x+_bXays~XxIayr0JlNis z*poE5{D7gXYGqX@-$QN0;INT2qXYI>RyJfY`wNo8A3}v7ziyW z4YdOp?ZNgT$P#oNf;=N@A-Mr~yj_J(LU+$uFdLM!WPZ$H8v{;0w6nOOuk^XTp|vvp z5R}kej6#9ZdIvHE;E;G=%nQ$Ac-b+wdmCF>MZl}*dv5f^jk@+DSn2IA71nMol(=I}XfMAD4##3QyI!Gp;sE62$q~t(&9UMI2)nlQqUr6I z3-<-}(1Fbd{D_tya_)oFqk1kKM~59?FBS?+6uIn6i{z{Lg7Wn>XsJTYU@v*RT7Iy7 zN+daOkHui!npmH#Pn;*v601{)n28eppnoiQf+kA8_?kQYJOh zpf8yRg6O_2J(NdDiUX(#VvR;**>T|tx`flG5vX(GQ=bP9fcv28k{ZvYS20rrH zgT;Zq(kV!fZ@~5-mM9oxuNfCX8mquJ{>u8rj66b7}zoEPQ4eEG~^)nG7 zZN>@;{Y;}2>`%FZuYP=C(_vy_5#-p`J6kBVu~02T0mKz7=l=ftheekI6hMf6sPZ)m znus;js#rt9#2S(S&`@&@{zU;I4XXeq@Q+6Q0IUf(TDR}~IywI|Ab3;Ojc+VCGKR45 zO~YO#ze&~Lh3T6nOy9IwrRy_T6B7Kq{mO=6t_;cz-3*#%BmsH#%;Dk|%-knpMX0*{ z4W`xLkrdV}K=nv429V&NNK%-nCMbnJpY!gb2C0OYBKiE>FxA&Nn0DN1A}4IJ26#R` zyoS7qs(@+PeOsGg^aa_!_Ny>o`bFeV6Zp3TL?-Z0jmRCeBIzw+={-Ttyk(-}tweOZ z6|DoE=LUu;-Z@r48gMnYiyGz`(J(Mjs5#yNEsrxrqv|d&g(+z}`BI@4XccM0Z}t-H zqW#-f`i1O73EAFUA@8&&H^S~LJqOE;!dxX3rnqkyF`VxR87d`0a1oI-MFwxbYGW*p zkf2^rJeJx&70gWn^@1k*)=iM%iD!iHHi;5OBgxSQMqjWDxa#(|28`M64yek|xMYwY zBKi`wabHi7BKGdUz!q-?V0uQ;#FwYJ^-E8^tzX&>qinE!W3!UFfX7u@t*qk4RI!Am zVRKbp;F~HxvmvE6wgwUe-zlb|JVB{9lVw2SSJ?@kkgtB%rX&X2FU4E-Qa_@=Lu-fD zVh|N9?~1JH{Lq6fQ@(>*&!g`ck0$Ft3l3_=(VL~#&yxAb`7g=1?Kg2m)B4G;xgdXj zf;{k1y8Xv;utG)F`bpB2rs_+Ulcl{Rrak4*s}e=ovBUd7?L-p1w7rN^hdB`d;9^-n z*&axVxB{Z~_Wh*(DgXC^^M`hba`-mQu=d}H z<@4=*ltS~+1iu*KewNx_0U&f3?vkh4e@}u>2}B41G#1nUeIP-%p!H7TOan_`;h`;s zNPU$$uR*?V>b;Nh9<|0S--MimQMVZmXZu-5WAAQ8dhjb~ib(*L7y@6}xKG9v&!%Tx zKXn$Hh}`++4kC0>@dTScy1fXKksY z0xQbP*vhN2amWKxf;yVtlIXWBQc3b_!?5*Wh=f50pbg)Fb(AV(sPW~v)i$%;LdU`O zcM+^kfYalP!GU2M2W*q6Ay9gRH`?!|h~vBFM)bqD2KfDkyYKenbV8%P@7nHsSExl! zW;)+pQ|#H%11c)uV1x>E$C=_M`bsx?lwwu7F-<=V|9DO%Rs;xX2R#&+65yCpdw;33 z9SGvT4$fS5(z6GC`qCu-J?{o9T7jk1D!xYxLShgG>i0fhya&+q(VHLa>_RY5=4U9C z&`5^%U}rnv(ft8*on5-*4#FOt5gv9>aYdT*b#O|lb5peWTLuEBXuqz4Vw1Q^5bv*) zZolot3Wf#1YMgX-sR%-ub!T<^bp&X3l**zT8EWs0@c6*wIY}Kb8E~8)0+QZu@8Ip{ zH%i11mDKja#Jk8a64}P7)I$^&FLmvUdWnM1yS5z}IrHZUh?HA@{^{Z-3!3Ig1{sv_(*beSld6ouh?H|y$8~Gulqa} zOMvy>w{8H{KO5*D*gU+BA)w@T1c(;>#w5JeA`V}nlp2b=*cwuWXc`8^I#SU7)yIHg zI3Un`QL90bO>xNpxhM?7K=J@&Rp%(GCVnx>?bLZ7&!^vNFD zN+XIqKI}X%0H7!SNn9EmM^4DF^4~<_^bU?8&3?SY5KQUz-3Q=Zipb@3YJqX!s2d^ApYsihrj(e&4MYX(Z47ip=rC*f@m^f9JKb8`}IIY-j)BL~%2A_I2tqNMVm|q!h_W7-AJ-Dzf%|p#cm-Z4L|r`edh}4?F%U z70x&kQs3Dxe%DStsTXxIupXY6INc0aV^ciAcU}~N$Cq9q5zXQO1MkM!4b&;V%Hm)s zQP=?z4lyhzMx0j2Y|A*)qT1>#hb3-Ck2t@P!-w^Yr1XRL>vW;I|2KH24vvIP$W+o7 zX%FV$a4{iUy^`4;M%Y*7aVJ|#ab=IIe z==W_{r9kYH1GqQHK`0D2(}QpQDF{^=n55MHj&0WQ>25AP5Jy?mk6FsP(59 z;ttP+JL27=JZdWx5$9{|w<{Tsd;l@p-_=quINq z&X=H{;GMMYj_oVJc%rIRAYnhOY{L*f+>mZB9+fS@ViD?6gf)7XA`~PpMJO8Vd>p&I zasm2NFHixJgzI~VC0VMjfvbW1)76TCK*z041?Zmxh2GikysG%$EU`}Bjlq7ET5k$* zzUn2ZkKdtF!e8juF^9Qhp%vT_VR+_MN~;cz@^NLW{SN#L1$PM7PjMYeknee{{f?Zx z5_S!YNt8sxm8wP%ti*+g&JX3vR0rB*u!WO?4o|!WJKq!c=MQ{gn;K`Z^9P7Qlsee1 zyL}R_Rja%GmN(nlP(g1=zt^St>zKUr144bTD3>3XI>&`2Q9>!J=<#?)k-~}!_S?^X zp6rp(NN?xVZm|k~401teS1zN-jYS{Q!Kk}H$0R+H`JZu?Qs+kspEmdLObQvWlW``- zI*pUCzCPS`g-rPlj`b5(JN*fTULl4s6i(pCJZYk%Klt*l=-jMx&bE?0I_E|iZf^Zp zI`|l*jeu{sh5$O!5~!eyN|OxeF-pkac+&NDC}AgEs6gRrII z05cle0O&K`m;o@jvPHSeghnmycu@|3)r5IRk2FZCDlll&3dJh@;%bHt@Q#^C&`P8V zVgOMliG{p_QO!WrwR*yob$?#e;Eyq^zXk;gfbInj-x^Z%2>l{CMg@)Uas5^U^{ptn zy9G${)k;RdkhW$*x(li_IoNqFGDzP-*4FSAI9Arc1f(Ig74#WtTh8a-+QJ3K1#*ot zspc&kzZeWd06WjYIvQGgh9R7m=Ms?$r4G-+5rXV6n5W4QjVyKE_qFd+h*0al9g$>W z1^d`y#}2GJAqv>}$zbONJ1l^G14>B91rb8jdAA7Lg`A!Acism$XzRt{`$lW;TXTSh z=8y<0IAZVI#Wg2D5Xu`uFxNB~(~dqjUwdjHFZ3w;t=`wp59lwN+!NE5pE1uqZ; zGlsbrs>)b)CV{|2`yFzaV1@~3k`Ql3Y;Ty~XrLU(MBr+nO7zD+EFP=}XsP2k zCZV#l#@SdJ(aSr7ow=la=SQJ^=SSAQ^P|unEjn|?c|XE*dX*QVFwfZ`@u5fiRots0 z@zKNg@i>*1aCC7Qu(RtGxBRdX- z6G*q}4V?5r;P|7=SW~fHPZi;nX6B4e=BR?(xS!x97ZT!pz0-PGxengf zdomd*bFC0elr43Bfe916UF!UOiLrOwlM&Hn`aHD2D5(1-s1dXUi4tYTqYQbw{U(SD z(9?V}!NAL(h^Qv{k0sFwYcbfl;!WKIsvg?KNiJJEuv0b-y9HQ&kxN*@y|$Kt60Pj? zy5)fE_A7k8CtX2%cIAx$&SMx@8dL8%sVo!blVFo2ze?d=9X5u$Z(^6;A z@3LmpAn*d5^2X0kfI2PDf_y}PEV#G){q6m(dmuQvJ94i3f>ckMjrmGjD=;De)}zH?jS&;WB~y%(G->%nvY86`-F zvAwd1b!zagtfxTs5Q#wcX`5hMm;eZn38aNTG*kFd5#s!+1Z|(NeAIeFer1yw?JJw) zrqW#oBn+FkOXBMNCFUjjuiy~tH6zy*G(_~@E7p~vq>W92=7XIt?-r8!E6yl~1%oR) zWiS@-ALB@Jq7B|aoplxnzXJV=YVF87QU=#o_VIpJ`*Q#l4S^|M!}voWpM_t3;_pVo^OoBGN9ezBGQP5p8OV-hPuKf%xM8w0Vg@|v4Hh$I7dXV9FsHkP;n5w{(!^D)H{Im%7@JV z-4ooY&L?4WXiIUakc6BRbXp#AHOi}{`i>r}z-)MJgJ%eYp$ga_is8_l1?lm)W9cF| zmys{_2MavW0$80l#7KfT2qF*D$+~Io88wL|hYCm_sCypf0u^KjX~QwVy@`Q?EQ|QB z1CRVQYy!CFjSci}aZ1NWiHy)?W4|Y+z!7zLI!D-i@a5+t!j$P_!jys}&>*OgA~@Di zFA}TsVpvTsfI@l0v)+vH_dWW+9nj3XI(_>c#8ZKrM@pSXRnE&Yuo8GP30=LOL;*Ck zj6pFi2HAqKs zbK*bBkZ+?257QHsDRle&zSsYs;;ue6j^n!TQX);!HZ9Sz5(id$zQ{2ZnRomWO)5v) zv?xY2NQo9H+0F;(<=r0dq`TYm?jFU9%;rcLMFgaP1K5BKq(F?ih>OCF1-NK}x`>V1 zsNLGAjm9kkRA>YwK=MaETBJb#NSgkBZ+7h?vKM=b*z2lvSQ|;IP_>Vh&HhJK__P_bk7tY)|_=8LB)o)$s zyuyr-9E- zlR*oE^2ZG06U}~Ekxx?F1|9-2TH4xowC(9`2YiKF9@#uQB$f&L2iePykJjQM$o3&aPdz_%7493H}uY|EDSYR=!jt#TUqJHx!zmN2>tqJ*I#O(LtSOx{8i zS9RXnF6Qk=lFeedz+@kT!!2#RPrr5tgM$HGodPI>k1!(vnVpcFsQ8bWyB9(27Yu&I zB$WV{)7b(B_8K0u>{@%6Nv>DcMs-2$T_73OsWl5Jj0cSJFl#Mn4bSJ}Vzk1yzm@1x z0n4yEb?z^20q_vmrRqbln16N@^w8ZdONj=(uW+re_gJrZJlKahU?Gl4~ zgQp>YPYO_v{1Lhyy;KLHu8REs`X*8pxi;F;b|YGn;X6&t&jt*Gu0ZN!csdjV&B)q3 zgJ+OGi$6)cEu_9#Z~RS^JVY@;&Pq&$Kp-%;DEk6Y_?ll_Y?BtK1Xoe|2uf?e){WMv z#A{a=yiS#qYmH$c;NdA&0ss_hX9!tCB*zFwJI`?ud$KNe+%ZDAB1!H%vHWRdm5B*7 zgMXxaX8$+(kJ3fHzB@#2*Y}{10dD93bcLYL2XyRNhvIgvy`&qI=Z__!7TegFD4o!I zLF@`B=f_ynf?sHWfxwLmX09=K;}%s5LTb2q0AbZw{uYDp9nh$S+%3-AKM?_H zJP;S7_xP ztl&yrvlq|HO$3{ zT|l?URUdfiI@o)w^S7WX^tY3v2sSY+2}XGR}r4sE3#`klAbSP!`!B=|*g@;{_n zJx|NLml3S*4qAe{@jc2YJlSI5UagG{*|k?f?w=x5Ov_drVVY?ipJ-cDcT#Ie@#ASm zLKlUupcQq0t31EW=>vCzYwy)jhxy_XCo``J@%p?%v>NMJcIkTPI-^E!cE*V{xw4zm zimq>kR5gOu-XxosIr_c>)`KINfEtKe=J6L;0xd>bVC48ktD20xKvt_m+xI^wY5$P)iMNZf;|OL`U35KFf{D08aNsVo!c8f=r>qAq#xBKBkr z)6km@G}Yb%CboPRgI)&jhBUkz)B{KW_0zEIrvyE~;6EXVU_!VOM%Q1hM=y7Qm<>)U z;RR-n8mf7@$f>gnz9+2iIGunKon$-rGVlqa94yj7=rM6Ipse-=f{UEx*H;7z6VZCP zVP0Ani>n+egp(v!1M6ibA7$`Zq~kB{BEOXA#Xan)i=5KoN6?wKAX$5hNly512dJ0` zTWzhS3=MEGfJ?Jsk`v5&H{r=8J4d;T0l;3z+@e866Nq0y)pS>UpBP32N_G*gT(uiG zytsJ>gH}FQ%IC0eZtIBcz%T#E9k*TZ>tIU5Q^LH9+4U&=%}n4&V4yVL>3VoKoPy|+ zSFw(#XRY=%WJm>W{pG**j_}L@S?_hJ<1RP?;8#+c6*wU27vLrI8ZI(j#hFHcbQoIz z{EDn#{KHs#kek9*fKEKR@+#N|pjm7LHnx049fgy&dxJAJK04gFFV-61!4pC71Qr#Z z;2#PPThnMb;t-Q)bpNNQEM~+`x^#DGJe|iF*AEYUoCIdQ@*ahp6tx zx+n^iHPF{y*P@_28!DEucUD5G6Xv56u{TmZ7RaPsKgN9Ur&PY~HYSt4jzJg>}CI5n%(5DsM+}9|ukQVpw2fB=JU7nf{{J$OOg1uBk(U{SFKNQ)!cyl zClQgpLJg_CemHi6=JqxAVlmVjY&W~spM?ZuM~dw!R9vOjE;l$b#o9sm5U5fWY#I

V5&x|G8xCC?$h8_#8njP1qG0F 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 X#R%P8V+QPVdej{wQ>5t2wFdtOCGO1# diff --git a/jackify/frontends/cli/commands/manual_download_flow.py b/jackify/frontends/cli/commands/manual_download_flow.py new file mode 100644 index 0000000..63409cb --- /dev/null +++ b/jackify/frontends/cli/commands/manual_download_flow.py @@ -0,0 +1,479 @@ +""" +CLI manual download flow. + +Handles the interactive terminal experience when the engine emits a batch of +files requiring manual download. Uses the same backend services as the GUI path +(ManualDownloadManager, DownloadWatcherService, FileValidatorService) but +outputs status to the terminal and reads simple keyboard commands. +""" + +import os +import sys +import queue +import shutil +import threading +import logging +import time +from pathlib import Path +from typing import Callable, Optional + +from jackify.backend.services.manual_download_manager import ManualDownloadManager, DownloadItem +from jackify.backend.services.download_watcher_service import WatcherConfig +from jackify.backend.handlers.config_handler import ConfigHandler + +logger = logging.getLogger(__name__) + +def _fmt_size(n: int) -> str: + if n <= 0: + return '' + for unit in ('B', 'KB', 'MB', 'GB'): + if n < 1024: + return f"{n:.0f} {unit}" + n /= 1024 + return f"{n:.1f} TB" + + +class CliManualDownloadFlow: + """ + Blocking CLI flow for manual downloads. Returns when all items are done + (complete or skipped) and the continue command has been written to the + engine's stdin pipe. + """ + + def __init__( + self, + items: list[dict], + loop_iteration: int, + download_dir: Path, + stdin_write: Callable[[str], bool], + output_callback: Optional[Callable[[str], None]] = None, + concurrent_limit: int = 2, + ): + self._stdin_write = stdin_write + self._output = output_callback or print + self._done_event = threading.Event() + self._config_handler = ConfigHandler() + self._command_queue: queue.Queue[str] = queue.Queue() + self._last_rendered_snapshot: Optional[str] = None + self._last_render_time = 0.0 + self._completed_successfully = False + self._interactive_tty = bool(getattr(sys.__stdout__, "isatty", lambda: False)()) + self._terminal = sys.__stdout__ if self._interactive_tty else None + self._screen_lines = 0 + self._status_dirty = True + self._notices: list[str] = [] + self._startup_render_blocked = False + + configured_watch = self._config_handler.get("manual_download_watch_directory", None) + watch_dir = None + if configured_watch: + cfg_path = Path(str(configured_watch)).expanduser() + if cfg_path.is_dir(): + watch_dir = cfg_path + if watch_dir is None: + xdg_dl = os.environ.get('XDG_DOWNLOAD_DIR', '') + watch_dir = Path(xdg_dl) if (xdg_dl and Path(xdg_dl).is_dir()) else Path.home() / 'Downloads' + + self._manager = ManualDownloadManager( + modlist_download_dir=download_dir, + watch_directory=watch_dir, + concurrent_limit=concurrent_limit, + on_item_updated=self._on_item_updated, + on_send_continue=self._on_all_done, + on_all_done=self._on_all_done_counts, + ) + self._manager.load_items(items, loop_iteration) + self._total = len(self._manager.items) + self._watch_dir = watch_dir + + def run(self) -> bool: + """Block until all items are complete/skipped and continue is sent.""" + if not self._confirm_start(): + self._output("Manual download phase cancelled.") + return False + self._startup_render_blocked = True + try: + self._manager.start() + finally: + self._startup_render_blocked = False + self._render_status(force=True) + if sys.stdin.isatty(): + threading.Thread(target=self._read_commands, daemon=True).start() + else: + self._output("[interactive commands unavailable: non-interactive stdin]") + + while not self._done_event.is_set(): + self._handle_pending_commands() + self._render_status() + self._done_event.wait(timeout=0.25) + + self._manager.stop() + return self._completed_successfully + + def _on_item_updated(self, item: DownloadItem) -> None: + status = { + 'browser_opened': 'browser opened', + 'validating': 'validating...', + 'complete': '[OK]', + 'deferred': '[deferred]', + 'skipped': '[skipped]', + 'error': '[error]', + }.get(item.status, item.status) + if not self._interactive_tty or item.status in ('error',): + self._output(f" {status:>14} {item.file_name}") + if item.error_message: + self._emit_notice(f"reason: {item.error_message}") + self._status_dirty = True + if self._startup_render_blocked or self._interactive_tty: + return + self._render_status(force=True) + + def _on_all_done_counts(self, completed: int, skipped: int) -> None: + self._completed_successfully = completed == self._total and skipped == 0 + self._emit_notice(f"All downloads done: {completed} complete, {skipped} skipped") + self._emit_notice("Signalling engine to continue...") + + def _on_all_done(self) -> None: + self._stdin_write('{"command":"continue"}') + self._done_event.set() + + def _retry_deferred(self) -> None: + retried = 0 + with self._manager._lock: + for item in self._manager._items: + if item.status in ('deferred', 'skipped'): + item.status = 'pending' + item.needs_user_retry = False + item.error_message = None + retried += 1 + if retried == 0: + self._emit_notice("[no deferred items to retry]") + return + self._emit_notice(f"[retried {retried} deferred item(s)]") + self._manager._open_next_tabs() + + def _set_watch_folder(self, raw: str) -> None: + candidate = Path(raw).expanduser() + if not candidate.is_dir(): + self._emit_notice(f"[invalid directory: {candidate}]") + return + self._watch_dir = candidate + self._manager._watch_dir = candidate + self._manager._watcher._config.watch_directory = candidate + # Force a fresh scan baseline for the newly-selected directory. + self._manager._watcher._known = {} + try: + self._config_handler.set("manual_download_watch_directory", str(candidate)) + self._config_handler.save_config() + except Exception: + logger.debug("Could not persist manual_download_watch_directory", exc_info=True) + self._emit_notice(f"[watch folder set to {candidate}]") + self._status_dirty = True + self._render_status(force=True) + + def _set_concurrency(self, raw: str) -> None: + try: + value = int(raw) + except ValueError: + self._emit_notice(f"[invalid number: {raw}]") + return + value = max(1, min(5, value)) + self._manager.set_concurrent_limit(value) + try: + self._config_handler.set("manual_download_concurrent_limit", value) + self._config_handler.save_config() + except Exception: + logger.debug("Could not persist manual_download_concurrent_limit", exc_info=True) + self._emit_notice(f"[concurrency set to {value}]") + self._status_dirty = True + self._render_status(force=True) + + def _read_commands(self) -> None: + while not self._done_event.is_set(): + try: + line = sys.stdin.readline() + except Exception: + return + if not line: + return + self._command_queue.put(line.strip()) + + def _handle_pending_commands(self) -> None: + while True: + try: + command = self._command_queue.get_nowait() + except queue.Empty: + return + if not command: + continue + if self._handle_command(command): + return + + def _handle_command(self, command: str) -> bool: + parts = command.split(maxsplit=1) + action = parts[0].lower() + arg = parts[1] if len(parts) > 1 else "" + + if action == 'help': + self._print_help() + elif action in ('list', 'ls', 'status'): + self._render_status(force=True) + elif action == 'open': + self._open_item(arg) + elif action in ('defer', 'skip'): + self._defer_item(arg) + elif action == 'retry': + self._retry_deferred() + elif action == 'watch': + if not arg: + self._emit_notice(f"[watch folder: {self._watch_dir}]") + else: + self._set_watch_folder(arg) + elif action == 'pause': + self._manager.pause() + self._emit_notice("[paused]") + elif action == 'resume': + self._manager.resume() + self._emit_notice("[resumed]") + elif action in ('concurrency', 'tabs'): + if not arg: + self._emit_notice(f"[concurrency: {self._manager._limit}]") + else: + self._set_concurrency(arg) + elif action in ('quit', 'exit'): + self._emit_notice("Stopping - downloaded files are preserved for resume.") + self._manager.stop() + self._done_event.set() + return True + else: + self._emit_notice(f"[unknown command: {command}]") + self._print_help() + return False + + def _print_help(self) -> None: + self._output("") + self._output("Commands:") + self._output(" list Show current status") + self._output(" open Re-open a file in the browser") + self._output(" defer Defer an active file") + self._output(" retry Retry all deferred files") + self._output(" watch Change watched download folder") + self._output(" pause | resume Pause or resume auto-open") + self._output(" concurrency <1-5> Set concurrent browser tabs") + self._output(" quit Stop and preserve progress") + self._output("") + + def _render_status(self, force: bool = False) -> None: + now = time.monotonic() + if not force and not self._status_dirty and (now - self._last_render_time) < 2.0: + return + with self._manager._lock: + items = list(self._manager._items) + complete = sum(1 for item in items if item.status == 'complete') + deferred = sum(1 for item in items if item.status in ('deferred', 'skipped')) + active = sum(1 for item in items if item.status == 'browser_opened') + validating = sum(1 for item in items if item.status == 'validating') + pending = sum(1 for item in items if item.status == 'pending') + paused = self._manager._paused + remaining = self._total - complete - deferred + snapshot = ( + f"Watch: {self._watch_dir} | Complete: {complete}/{self._total} | " + f"Active: {active} | Validating: {validating} | Pending: {pending} | " + f"Deferred: {deferred} | Remaining: {remaining} | " + f"Tabs: {self._manager._limit} | {'Paused' if paused else 'Running'}" + ) + if not force and snapshot == self._last_rendered_snapshot: + return + self._last_rendered_snapshot = snapshot + self._last_render_time = now + self._status_dirty = False + recheck_note = None + if self._manager._items: + first = self._manager._items[0] + if first.loop_iteration > 1: + recheck_note = f"Recheck {first.loop_iteration} - still missing: {self._total}" + lines = [ + "", + *self._boxed_lines( + "Jackify CLI Download Manager", + [ + f"Files required: {self._total}", + f"Concurrent browser tabs: {self._manager._limit}", + f"Watching: {self._watch_dir}", + *( [recheck_note] if recheck_note else [] ), + ], + ), + *self._boxed_lines( + "Action Required", + [ + "Check your browser now.", + "Jackify may have opened Nexus download pages in the background.", + "Type `help` at any time for available commands.", + ], + ), + *self._boxed_lines("Status", [snapshot]), + *self._boxed_lines( + "Downloads", + [self._format_table_header(), *[self._format_item(item) for item in self._visible_items(items)]], + ), + *self._boxed_lines( + "Commands", + ["help | list | open | defer | retry | watch | pause | resume | concurrency <1-5> | quit"], + ), + *self._boxed_lines("Notices", self._notices[-3:] or ["None"]), + "", + ] + if self._interactive_tty and self._terminal is not None: + self._redraw_terminal(lines) + else: + for line in lines: + self._output(line) + + def _visible_items(self, items: list[DownloadItem]) -> list[DownloadItem]: + priority = {'browser_opened': 0, 'validating': 1, 'pending': 2, 'deferred': 3, 'skipped': 4, 'error': 5, 'complete': 6} + return sorted(items, key=lambda item: (priority.get(item.status, 9), item.index))[:14] + + def _format_item(self, item: DownloadItem) -> str: + status = { + 'browser_opened': 'OPEN ', + 'validating': 'CHECK', + 'pending': 'WAIT ', + 'deferred': 'DEFER', + 'skipped': 'SKIP ', + 'error': 'ERROR', + 'complete': 'DONE ', + }.get(item.status, item.status[:5].upper()) + size = _fmt_size(item.expected_size).rjust(7) if item.expected_size else ' ' * 7 + mod_name = self._truncate(item.mod_name or "", 24).ljust(24) + file_name = self._truncate(item.file_name, 32).ljust(32) + return f"{item.index:>3}/{item.total:<3} {status} {size} {mod_name} {file_name}" + + def _find_item(self, raw: str) -> Optional[DownloadItem]: + if not raw: + self._emit_notice("[missing item index]") + return None + if raw.isdigit(): + target_index = int(raw) + for item in self._manager.items: + if item.index == target_index: + return item + low = raw.lower() + for item in self._manager.items: + if item.file_name.lower() == low: + return item + self._emit_notice(f"[item not found: {raw}]") + return None + + def _open_item(self, raw: str) -> None: + item = self._find_item(raw) + if not item: + return + if self._manager.reopen_item(item.file_name): + self._emit_notice(f"[opened] {item.file_name}") + else: + self._emit_notice(f"[could not open] {item.file_name}") + + def _defer_item(self, raw: str) -> None: + item = self._find_item(raw) + if not item: + return + if item.status not in ('browser_opened', 'pending', 'error'): + self._emit_notice(f"[cannot defer item in state: {item.status}]") + return + self._manager.skip_item(item.file_name) + self._emit_notice(f"[deferred] {item.file_name}") + self._status_dirty = True + + def _confirm_start(self) -> bool: + self._output("") + for line in self._boxed_lines( + "Non-Premium Manual Download Flow", + [ + "Jackify detected that manual Nexus downloads are required.", + "It will open Nexus pages in your browser, watch your download folder,", + "validate files automatically, and resume once everything is present.", + "", + "Key commands:", + "concurrency 1-5 Change simultaneous browser tabs", + "defer Skip an item for now", + "retry Reopen deferred items", + "watch Change monitored download folder", + ], + ): + self._output(line) + if not sys.stdin.isatty(): + return True + self._output("Continue? [Y/n]: ") + try: + response = (sys.stdin.readline() or "").strip().lower() + except Exception: + return False + return response in ("", "y", "yes") + + def _redraw_terminal(self, lines: list[str]) -> None: + if self._terminal is None: + return + terminal_width = self._terminal_columns() + if self._screen_lines: + self._terminal.write(f"\x1b[{self._screen_lines}F") + self._terminal.write("\x1b[J") + for line in lines: + self._terminal.write(line[: max(1, terminal_width - 1)] + "\n") + self._terminal.flush() + self._screen_lines = len(lines) + + @staticmethod + def _truncate(value: str, width: int) -> str: + if len(value) <= width: + return value + if width <= 3: + return value[:width] + return value[: width - 3] + "..." + + @staticmethod + def _format_table_header() -> str: + return "Idx State Size Mod File" + + def _boxed_lines(self, title: str, rows: list[str], width: Optional[int] = None) -> list[str]: + width = width or max(60, min(100, self._terminal_columns() - 2)) + inner_width = max(20, width - 4) + top = "+" + "-" * (width - 2) + "+" + rendered = [top, f"| {self._truncate(title, inner_width).ljust(inner_width)} |"] + if rows: + rendered.append("|" + "-" * (width - 2) + "|") + for row in rows: + rendered.append(f"| {self._truncate(row, inner_width).ljust(inner_width)} |") + rendered.append(top) + return rendered + + def _terminal_columns(self) -> int: + return shutil.get_terminal_size(fallback=(100, 24)).columns + + def _emit_notice(self, message: str) -> None: + if self._interactive_tty: + self._notices.append(message) + self._status_dirty = True + return + self._output(message) + +def run_cli_manual_download_phase( + events: list[dict], + loop_iteration: int, + download_dir: Path, + stdin_write: Callable[[str], bool], + output_callback: Optional[Callable[[str], None]] = None, + concurrent_limit: int = 2, +) -> bool: + """ + Entry point called from modlist_service_installation when the engine emits + a manual_download_list_complete event. Blocks until done. + """ + flow = CliManualDownloadFlow( + items=events, + loop_iteration=loop_iteration, + download_dir=download_dir, + stdin_write=stdin_write, + output_callback=output_callback, + concurrent_limit=concurrent_limit, + ) + return flow.run() diff --git a/jackify/frontends/cli/commands/vnv_manual_downloads.py b/jackify/frontends/cli/commands/vnv_manual_downloads.py new file mode 100644 index 0000000..f0ae0bc --- /dev/null +++ b/jackify/frontends/cli/commands/vnv_manual_downloads.py @@ -0,0 +1,140 @@ +"""CLI helpers for VNV manual-download handling.""" + +from pathlib import Path +from typing import Callable, Optional + +from jackify.backend.services.nexus_premium_service import NexusPremiumService +from jackify.backend.services.vnv_post_install_service import VNVPostInstallService +from jackify.frontends.cli.commands.manual_download_flow import run_cli_manual_download_phase +from jackify.frontends.cli.ui.indeterminate_status import CliIndeterminateStatus + + +def _is_explicitly_non_premium(service: VNVPostInstallService) -> bool: + auth_token = service.auth_service.get_auth_token() + auth_method = service.auth_service.get_auth_method() + if not auth_token or not auth_method: + return False + is_premium, username = NexusPremiumService().check_premium_status( + auth_token, + is_oauth=auth_method == "oauth", + ) + return username is not None and not is_premium + + +def _missing_manual_items(service: VNVPostInstallService) -> list[dict]: + completed = service.check_already_completed() + include_bsa = not completed["bsa_decompressed"] and not ( + service._find_cached_bsa_mpi() or service._find_cached_bsa_package() + ) + include_4gb = not completed["4gb_patch"] and not service._find_cached_4gb_patcher() + if not include_4gb and not include_bsa: + return [] + items = service.get_manual_download_items(include_bsa=include_bsa) + if include_4gb: + return items + return [item for item in items if int(item.get("mod_id", 0)) != service.LINUX_4GB_PATCHER_MOD_ID] + + +def ensure_vnv_cli_manual_downloads( + service: VNVPostInstallService, + output_callback: Optional[Callable[[str], None]] = None, +) -> bool: + if not _is_explicitly_non_premium(service): + return True + items = _missing_manual_items(service) + if not items: + return True + output = output_callback or print + output("") + output("VNV requires manual Nexus downloads for this account. Opening Jackify CLI Download Manager...") + return run_cli_manual_download_phase( + events=items, + loop_iteration=1, + download_dir=service.cache_dir, + stdin_write=lambda _payload: True, + output_callback=output, + concurrent_limit=2, + ) + + +def build_vnv_cli_manual_file_callback( + service: VNVPostInstallService, + output_callback: Optional[Callable[[str], None]] = None, +): + output = output_callback or print + manual_items = service.get_manual_download_items(include_bsa=True) + + def _cached_file_for_title(title: str) -> Optional[Path]: + if "4GB" in title: + return service._find_cached_4gb_patcher() + return service._find_cached_bsa_mpi() or service._find_cached_bsa_package() + + def _manual_file_callback(title: str, instructions: str) -> Optional[Path]: + cached = _cached_file_for_title(title) + if cached: + return cached + mod_id = ( + service.LINUX_4GB_PATCHER_MOD_ID + if "4GB" in title + else service.FNV_BSA_DECOMPRESSOR_MOD_ID + ) + item = next((entry for entry in manual_items if int(entry.get("mod_id", 0)) == mod_id), None) + if not item: + output("") + output(instructions) + return None + output("") + output(f"{title} - opening Jackify CLI Download Manager...") + success = run_cli_manual_download_phase( + events=[item], + loop_iteration=1, + download_dir=service.cache_dir, + stdin_write=lambda _payload: True, + output_callback=output, + concurrent_limit=1, + ) + if not success: + return None + return _cached_file_for_title(title) + + return _manual_file_callback + + +def create_vnv_cli_progress_callback( + output_callback: Optional[Callable[[str], None]] = None, +) -> tuple[Callable[[str], None], Callable[[], None]]: + """Create a CLI progress callback with a pulser for indeterminate VNV stages.""" + output = output_callback or print + pulser = CliIndeterminateStatus() + + def _should_pulse(message: str) -> bool: + lowered = message.lower() + if "%" in lowered: + return False + if "assets processed:" in lowered: + return False + if "decompressing bsa files:" in lowered: + return False + pulse_markers = ( + "running vnv post-install automation", + "running bsa decompressor", + "running 4gb patcher", + "preparing bsa decompressor package", + "extracting bsa package", + "ensuring ttw_linux_installer is available", + "checking for post-install automation", + "finalizing post-install configuration", + ) + return any(marker in lowered for marker in pulse_markers) + + def _progress(message: str) -> None: + text = (message or "").strip() + if not text: + return + if _should_pulse(text): + pulser.set(text) + return + pulser.stop() + output(text) + + return _progress, pulser.close diff --git a/jackify/frontends/cli/menus/additional_menu.py b/jackify/frontends/cli/menus/additional_menu.py index 299a86b..4d25b48 100644 --- a/jackify/frontends/cli/menus/additional_menu.py +++ b/jackify/frontends/cli/menus/additional_menu.py @@ -150,16 +150,103 @@ class AdditionalMenuHandler: else: output_path = Path(output_path).expanduser() + # Check if output directory already has content — mirror GUI behaviour + if output_path.exists() and output_path.is_dir(): + try: + has_files = any(output_path.iterdir()) + except Exception: + has_files = False + if has_files: + print(f"\n{COLOR_WARNING}The TTW output directory already exists and contains files:{COLOR_RESET}") + print(f" {output_path}") + print(f"{COLOR_WARNING}All files in this directory will be deleted before installation.{COLOR_RESET}") + print(f"{COLOR_WARNING}This action cannot be undone.{COLOR_RESET}") + confirm = input(f"{COLOR_PROMPT}Delete existing files and continue? (y/N): {COLOR_RESET}").strip().lower() + if confirm not in ('y', 'yes'): + print(f"{COLOR_INFO}TTW installation cancelled.{COLOR_RESET}") + input("Press Enter to return to menu...") + return + import shutil + try: + for item in output_path.iterdir(): + if item.is_dir(): + shutil.rmtree(item) + else: + item.unlink() + except Exception as e: + print(f"{COLOR_ERROR}Failed to clear output directory: {e}{COLOR_RESET}") + input("Press Enter to return to menu...") + return + # Run TTW installation + import re + + phase_state = {"current": "Processing", "last_rendered": ""} + progress_line_active = {"value": False} + + def _strip_ansi(text: str) -> str: + return re.sub(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])', '', text or '') + + def _ttw_output_callback(line: str): + clean = _strip_ansi(line or "").strip() + if not clean: + return + lower = clean.lower() + rendered = "" + manifest_match = re.search(r'loading manifest:\s*(\d+)/(\d+)', lower) + if manifest_match: + current = int(manifest_match.group(1)) + total = int(manifest_match.group(2)) + phase_state["current"] = "Loading manifest" + percent = int((current / total) * 100) if total > 0 else 0 + rendered = f"[TTW] {phase_state['current']}: {current:,}/{total:,} ({percent}%)" + else: + progress_match = re.search(r'\[(\d+)/(\d+)\]', clean) + if progress_match: + current = int(progress_match.group(1)) + total = int(progress_match.group(2)) + percent = int((current / total) * 100) if total > 0 else 0 + rendered = f"[TTW] {phase_state['current']}: {current:,}/{total:,} ({percent}%)" + else: + if 'manifest' in lower: + phase_state["current"] = "Loading manifest" + elif any(t in lower for t in ('extract', 'decompress', 'installing', 'copying', 'merge')): + phase_state["current"] = clean + is_milestone = any(t in lower for t in ('===', 'complete', 'finished', 'starting', 'valid')) + is_error = 'error:' in lower + is_warning = 'warning:' in lower + if is_milestone or is_error or is_warning: + rendered = f"[TTW] {clean}" + + if not rendered or rendered == phase_state["last_rendered"]: + return + phase_state["last_rendered"] = rendered + if re.search(r'^\[TTW\] .+?: [\d,]+/[\d,]+ \(\d+%\)$', rendered): + print(f"\r{COLOR_INFO}{rendered}{COLOR_RESET}", end="", flush=True) + progress_line_active["value"] = True + else: + if progress_line_active["value"]: + print() + progress_line_active["value"] = False + print(f"{COLOR_INFO}{rendered}{COLOR_RESET}") + print(f"\n{COLOR_INFO}Starting TTW installation workflow...{COLOR_RESET}") - success, message = ttw_installer_handler.install_ttw_backend(mpi_path, output_path) - + print(f"{COLOR_INFO}This may take 15-30 minutes.{COLOR_RESET}\n") + success, message = ttw_installer_handler.install_ttw_backend_with_output_stream( + mpi_path, output_path, output_callback=_ttw_output_callback + ) + if progress_line_active["value"]: + print() + if success: print(f"\n{COLOR_SUCCESS}TTW installation completed successfully!{COLOR_RESET}") print(f"{COLOR_INFO}TTW installed to: {output_path}{COLOR_RESET}") + print(f"{COLOR_INFO}Detailed log available at: ~/Jackify/logs/TTW_Install_workflow.log{COLOR_RESET}") + input("Press Enter to return to menu...") else: print(f"\n{COLOR_ERROR}TTW installation failed.{COLOR_RESET}") print(f"{COLOR_ERROR}Error: {message}{COLOR_RESET}") + print(f"{COLOR_INFO}Detailed log available at: ~/Jackify/logs/TTW_Install_workflow.log{COLOR_RESET}") input("Press Enter to return to menu...") def _execute_nexus_authorization(self, cli_instance): diff --git a/jackify/frontends/cli/ui/indeterminate_status.py b/jackify/frontends/cli/ui/indeterminate_status.py new file mode 100644 index 0000000..6ca12b5 --- /dev/null +++ b/jackify/frontends/cli/ui/indeterminate_status.py @@ -0,0 +1,70 @@ +"""Single-line CLI pulser for indeterminate background stages.""" + +from __future__ import annotations + +import itertools +import sys +import threading +import time +from typing import Optional + + +class CliIndeterminateStatus: + """Render one in-place pulsing status line for long-running CLI steps.""" + + def __init__(self, output=None, interval: float = 0.12): + self._output = output or sys.stdout + self._interval = interval + self._interactive = bool(getattr(self._output, "isatty", lambda: False)()) + self._message: Optional[str] = None + self._printed_message: Optional[str] = None + self._stop_event = threading.Event() + self._lock = threading.Lock() + self._thread: Optional[threading.Thread] = None + + def set(self, message: str) -> None: + """Start or update the active pulsing message.""" + cleaned = (message or "").strip() + if not cleaned: + self.stop() + return + if not self._interactive: + if cleaned != self._printed_message: + print(cleaned, file=self._output, flush=True) + self._printed_message = cleaned + return + with self._lock: + self._message = cleaned + if self._thread and self._thread.is_alive(): + return + self._stop_event.clear() + self._thread = threading.Thread(target=self._run, daemon=True) + self._thread.start() + + def stop(self) -> None: + """Stop the pulser and clear its terminal line.""" + if not self._interactive: + return + self._stop_event.set() + if self._thread and self._thread.is_alive(): + self._thread.join(timeout=0.5) + self._thread = None + with self._lock: + self._message = None + self._output.write("\r\033[2K") + self._output.flush() + + def close(self) -> None: + self.stop() + + def _run(self) -> None: + for frame in itertools.cycle("|/-\\"): + if self._stop_event.wait(self._interval): + return + with self._lock: + message = self._message + if not message: + continue + self._output.write(f"\r\033[2K{message} {frame}") + self._output.flush() + diff --git a/jackify/frontends/gui/dialogs/existing_setup_dialog.py b/jackify/frontends/gui/dialogs/existing_setup_dialog.py new file mode 100644 index 0000000..e58327a --- /dev/null +++ b/jackify/frontends/gui/dialogs/existing_setup_dialog.py @@ -0,0 +1,204 @@ +"""Shared dialog for existing install/shortcut detection decisions.""" + +from __future__ import annotations + +from typing import Optional, Tuple + +from PySide6.QtCore import Qt +from PySide6.QtWidgets import ( + QDialog, + QFrame, + QHBoxLayout, + QLabel, + QLineEdit, + QPushButton, + QSizePolicy, + QVBoxLayout, + QWidget, +) + + +def prompt_existing_setup_dialog( + parent: QWidget, + *, + window_title: str, + heading: str, + body: str, + existing_name: str, + requested_name: str, + install_dir: Optional[str] = None, + field_label: str = "New shortcut name", + reuse_label: str = "Use Existing Setup", + new_label: str = "Create New Shortcut", + cancel_label: str = "Cancel", +) -> Tuple[str, Optional[str]]: + """ + Show the shared existing-setup dialog. + + Returns: + ("reuse"|"new"|"cancel", new_name_or_none) + """ + dialog = QDialog(parent) + dialog.setWindowTitle(window_title) + dialog.setModal(True) + dialog.setMinimumWidth(760) + dialog.setMinimumHeight(320) + + dialog.setStyleSheet( + """ + QDialog { + background: #181818; + color: #ffffff; + border-radius: 12px; + } + QFrame#dialogCard { + background: #23272e; + border: 1px solid #353a40; + border-radius: 12px; + } + QFrame#infoCard { + background: #2a2f36; + border: 1px solid #3b4148; + border-radius: 8px; + } + QLabel { + color: #ffffff; + font-size: 14px; + padding: 0px; + } + QLabel#dialogTitle { + font-size: 22px; + font-weight: 600; + color: #3fb7d6; + } + QLabel#dialogBody { + color: #e0e0e0; + line-height: 1.35; + } + QLabel#infoLabel { + color: #c7d0d8; + font-size: 13px; + line-height: 1.3; + } + QLabel#fieldLabel { + color: #b0b0b0; + font-size: 12px; + } + QLineEdit { + background-color: #404040; + color: #ffffff; + border: 2px solid #555555; + border-radius: 4px; + padding: 8px; + font-size: 14px; + selection-background-color: #3fd0ea; + } + QLineEdit:focus { + border-color: #3fd0ea; + } + QPushButton { + background-color: #404040; + color: #ffffff; + border: 2px solid #555555; + border-radius: 4px; + padding: 8px 16px; + font-size: 14px; + min-width: 120px; + } + QPushButton:hover { + background-color: #505050; + border-color: #3fd0ea; + } + QPushButton:pressed { + background-color: #303030; + } + """ + ) + + outer_layout = QVBoxLayout(dialog) + outer_layout.setContentsMargins(24, 20, 24, 20) + outer_layout.setSpacing(0) + + card = QFrame(dialog) + card.setObjectName("dialogCard") + card_layout = QVBoxLayout(card) + card_layout.setContentsMargins(22, 22, 22, 22) + card_layout.setSpacing(14) + + title_label = QLabel(heading) + title_label.setObjectName("dialogTitle") + title_label.setAlignment(Qt.AlignCenter) + title_label.setWordWrap(True) + card_layout.addWidget(title_label) + + body_label = QLabel(body) + body_label.setObjectName("dialogBody") + body_label.setAlignment(Qt.AlignCenter) + body_label.setWordWrap(True) + card_layout.addWidget(body_label) + + info_card = QFrame(card) + info_card.setObjectName("infoCard") + info_layout = QVBoxLayout(info_card) + info_layout.setContentsMargins(14, 12, 14, 12) + info_layout.setSpacing(6) + + info_lines = [ + f"Existing shortcut: {existing_name}", + f"Requested name: {requested_name or existing_name}", + ] + if install_dir: + info_lines.append(f"Install directory: {install_dir}") + info_label = QLabel("
".join(info_lines)) + info_label.setObjectName("infoLabel") + info_label.setTextFormat(Qt.RichText) + info_label.setWordWrap(True) + info_layout.addWidget(info_label) + card_layout.addWidget(info_card) + + field_title = QLabel(field_label) + field_title.setObjectName("fieldLabel") + card_layout.addWidget(field_title) + + name_input = QLineEdit(requested_name or existing_name) + name_input.selectAll() + card_layout.addWidget(name_input) + + button_layout = QHBoxLayout() + button_layout.setSpacing(10) + + reuse_button = QPushButton(reuse_label) + cancel_button = QPushButton(cancel_label) + new_button = QPushButton(new_label) + for button in (reuse_button, cancel_button, new_button): + button.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + + button_layout.addWidget(reuse_button) + button_layout.addWidget(cancel_button) + button_layout.addWidget(new_button) + card_layout.addLayout(button_layout) + outer_layout.addWidget(card) + + result = {"action": "cancel", "new_name": None} + + def on_reuse(): + result["action"] = "reuse" + dialog.accept() + + def on_new(): + result["action"] = "new" + result["new_name"] = name_input.text().strip() + dialog.accept() + + def on_cancel(): + result["action"] = "cancel" + dialog.reject() + + reuse_button.clicked.connect(on_reuse) + new_button.clicked.connect(on_new) + cancel_button.clicked.connect(on_cancel) + name_input.returnPressed.connect(on_new) + + dialog.adjustSize() + dialog.exec() + return result["action"], result["new_name"] diff --git a/jackify/frontends/gui/dialogs/manual_download_dialog.py b/jackify/frontends/gui/dialogs/manual_download_dialog.py new file mode 100644 index 0000000..382ac8f --- /dev/null +++ b/jackify/frontends/gui/dialogs/manual_download_dialog.py @@ -0,0 +1,466 @@ +""" +Manual Download Dialog + +Shown when the engine requires manual downloads (non-premium or forced-manual +archives). Displays all pending items in a scrollable table, manages browser +tab concurrency, and coordinates with ManualDownloadManager. +""" + +import logging +from pathlib import Path +from typing import Optional + +from PySide6.QtCore import Qt, Signal, QObject +from PySide6.QtWidgets import ( + QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, + QSpinBox, QFrame, QTableWidget, QTableWidgetItem, QHeaderView, + QProgressBar, QFileDialog, QSizePolicy, +) +from PySide6.QtGui import QFont + +from jackify.backend.services.manual_download_manager import ManualDownloadManager, DownloadItem +from jackify.backend.handlers.config_handler import ConfigHandler +from jackify.frontends.gui.shared_theme import JACKIFY_COLOR_BLUE + +logger = logging.getLogger(__name__) + +_STATUS_LABELS = { + 'pending': 'Pending', + 'browser_opened': 'Browser Opened', + 'validating': 'Validating...', + 'complete': 'Complete', + 'deferred': 'Deferred', + 'skipped': 'Skipped', + 'error': 'Error', +} + +_STATUS_COLOURS = { + 'pending': '#808080', + 'browser_opened': '#3498db', + 'validating': '#f39c12', + 'complete': '#27ae60', + 'deferred': '#e67e22', + 'skipped': '#e67e22', + 'error': '#e74c3c', +} + +# Column indices +_COL_MOD = 0 +_COL_NAME = 1 +_COL_SIZE = 2 +_COL_STATUS = 3 + + +def _fmt_size(n: int) -> str: + if n <= 0: + return '' + for unit in ('B', 'KB', 'MB', 'GB'): + if n < 1024: + return f"{n:.0f} {unit}" + n /= 1024 + return f"{n:.1f} TB" + + +class _Bridge(QObject): + """Tiny bridge so worker-thread callbacks can update the Qt table safely.""" + item_updated = Signal(object) # DownloadItem + all_done = Signal(int, int) # completed, skipped + + +class ManualDownloadDialog(QDialog): + """ + Displays all pending manual downloads and coordinates the download workflow. + Non-modal so the install log remains visible. + """ + + def __init__( + self, + manager: ManualDownloadManager, + modlist_name: str = '', + watch_directory: Optional[Path] = None, + concurrent_limit: int = 2, + parent=None, + ): + super().__init__(parent) + self._manager = manager + self._modlist_name = modlist_name + self._watch_dir = watch_directory or (Path.home() / 'Downloads') + self._paused = False + self._started = False + self._initial_concurrent_limit = max(1, min(5, int(concurrent_limit))) + + # Row index by file_name for fast updates + self._row_map: dict[str, int] = {} + + # Bridge for thread-safe table updates + self._bridge = _Bridge() + self._bridge.item_updated.connect(self._on_item_updated_slot) + self._bridge.all_done.connect(self._on_all_done_slot) + + # Preserve any existing manager callbacks so workflow controllers still + # receive completion events after the dialog updates its own UI. + prev_item_updated = self._manager._on_item_updated + prev_all_done = self._manager._on_all_done + + def _emit_item_updated(item): + self._bridge.item_updated.emit(item) + if prev_item_updated: + prev_item_updated(item) + + def _emit_all_done(completed: int, skipped: int): + self._bridge.all_done.emit(completed, skipped) + if prev_all_done: + prev_all_done(completed, skipped) + + self._manager._on_item_updated = _emit_item_updated + self._manager._on_all_done = _emit_all_done + + self.setWindowTitle("Manual Downloads Required") + self.setMinimumSize(760, 500) + self.setModal(False) + self._build_ui() + + # ------------------------------------------------------------------ + # Public + # ------------------------------------------------------------------ + + def showEvent(self, event) -> None: + super().showEvent(event) + if not self._started: + # Keep the workflow idle until the user explicitly clicks Start. + # Start backend services in paused mode so watcher/precheck are ready + # without opening browser tabs yet. + self._paused = False + self._manager.pause() + self._manager.start() + self._start_pause_btn.setText("Start") + self._progress_label.setText("Ready - click Start to begin opening download tabs") + + def load_items(self, items: list[DownloadItem]) -> None: + """ + Populate or refresh the table from a list of DownloadItems. + On subsequent loop iterations the manager passes its full item list + (including previously-completed rows), so we update existing rows and + append only genuinely new ones rather than rebuilding the table. + """ + new_items = [i for i in items if i.file_name not in self._row_map] + existing_items = [i for i in items if i.file_name in self._row_map] + + # Update existing rows without disabling updates (usually few on repeat iterations) + for item in existing_items: + self._update_row(self._row_map[item.file_name], item) + + # Batch-insert new rows with viewport updates suspended to avoid O(n²) repaints + if new_items: + self._table.setUpdatesEnabled(False) + try: + start_row = self._table.rowCount() + self._table.setRowCount(start_row + len(new_items)) + for i, item in enumerate(new_items): + self._fill_row(start_row + i, item) + self._row_map[item.file_name] = start_row + i + finally: + self._table.setUpdatesEnabled(True) + self._table.viewport().update() + + self._refresh_header() + # If user already started the workflow and engine enters another manual loop, + # continue opening tabs for newly-pending items automatically. + if self._started and not self._paused: + self._manager.resume() + + def update_item(self, item: DownloadItem) -> None: + """Called from any thread - bridges to Qt slot.""" + self._bridge.item_updated.emit(item) + + # ------------------------------------------------------------------ + # Build UI + # ------------------------------------------------------------------ + + def _build_ui(self) -> None: + root = QVBoxLayout(self) + root.setContentsMargins(16, 16, 16, 16) + root.setSpacing(10) + + # Header + hdr = QFrame() + hdr.setFrameShape(QFrame.StyledPanel) + hdr.setStyleSheet("QFrame { background: #1e2228; border-radius: 8px; border: 1px solid #333; }") + hdr_layout = QVBoxLayout(hdr) + hdr_layout.setContentsMargins(12, 10, 12, 10) + hdr_layout.setSpacing(6) + + self._title_label = QLabel(f"Modlist: {self._modlist_name or 'Unknown'}") + self._title_label.setStyleSheet("color: #e0e0e0; font-size: 14px; font-weight: 600;") + hdr_layout.addWidget(self._title_label) + + self._progress_label = QLabel("Preparing...") + self._progress_label.setStyleSheet("color: #aaaaaa; font-size: 12px;") + hdr_layout.addWidget(self._progress_label) + + self._progress_bar = QProgressBar() + self._progress_bar.setRange(0, 100) + self._progress_bar.setValue(0) + self._progress_bar.setStyleSheet( + f"QProgressBar {{ border: 1px solid #444; border-radius: 4px; background: #2c2c2c; " + f"height: 12px; color: #d7e3f4; font-weight: 600; }}" + f"QProgressBar::chunk {{ background: {JACKIFY_COLOR_BLUE}; border-radius: 3px; }}" + ) + hdr_layout.addWidget(self._progress_bar) + root.addWidget(hdr) + + # Table + self._table = QTableWidget() + self._table.setColumnCount(4) + self._table.setHorizontalHeaderLabels(['Mod', 'File', 'Size', 'Status']) + self._table.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch) + self._table.horizontalHeader().setSectionResizeMode(1, QHeaderView.Stretch) + self._table.horizontalHeader().setSectionResizeMode(2, QHeaderView.Fixed) + self._table.horizontalHeader().setSectionResizeMode(3, QHeaderView.Fixed) + self._table.setColumnWidth(2, 90) + self._table.setColumnWidth(3, 130) + self._table.setSelectionBehavior(QTableWidget.SelectRows) + self._table.setEditTriggers(QTableWidget.NoEditTriggers) + self._table.setAlternatingRowColors(True) + self._table.verticalHeader().setVisible(False) + self._table.cellDoubleClicked.connect(self._on_row_double_clicked) + self._table.setStyleSheet( + "QTableWidget { background: #1a1d23; alternate-background-color: #1e2228; " + "color: #d0d0d0; gridline-color: #333; border: 1px solid #333; border-radius: 4px; }" + "QHeaderView::section { background: #252830; color: #aaa; border: none; " + "padding: 4px; font-size: 11px; }" + ) + root.addWidget(self._table) + + # Controls row + ctrl = QHBoxLayout() + ctrl.setSpacing(12) + + ctrl.addWidget(QLabel("Concurrent tabs:")) + self._concurrent_spin = QSpinBox() + self._concurrent_spin.setRange(1, 5) + self._concurrent_spin.setValue(self._initial_concurrent_limit) + self._concurrent_spin.setFixedWidth(60) + self._concurrent_spin.valueChanged.connect(self._on_concurrent_changed) + ctrl.addWidget(self._concurrent_spin) + + ctrl.addSpacing(16) + ctrl.addWidget(QLabel("Watch folder:")) + self._folder_label = QLabel(str(self._watch_dir)) + self._folder_label.setStyleSheet("color: #aaa; font-size: 11px;") + self._folder_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) + ctrl.addWidget(self._folder_label) + + folder_btn = QPushButton("...") + folder_btn.setFixedSize(32, 28) + folder_btn.clicked.connect(self._on_pick_folder) + ctrl.addWidget(folder_btn) + + root.addLayout(ctrl) + + watch_hint = QLabel( + "Jackify watches this folder for newly downloaded archives, validates them, " + "then moves valid files into your modlist downloads folder automatically. " + "Double-click a row (or use Open Selected) to reopen a URL if you closed a tab." + ) + watch_hint.setWordWrap(True) + watch_hint.setStyleSheet("color: #8f98a3; font-size: 11px;") + root.addWidget(watch_hint) + + # Action buttons + btn_row = QHBoxLayout() + btn_row.setSpacing(10) + + self._retry_btn = QPushButton("Retry Deferred (0)") + self._retry_btn.setEnabled(False) + self._retry_btn.clicked.connect(self._on_retry_skipped) + btn_row.addWidget(self._retry_btn) + + self._defer_btn = QPushButton("Defer Selected") + self._defer_btn.clicked.connect(self._on_defer_selected) + btn_row.addWidget(self._defer_btn) + + self._open_selected_btn = QPushButton("Open Selected") + self._open_selected_btn.clicked.connect(self._on_open_selected) + btn_row.addWidget(self._open_selected_btn) + + btn_row.addStretch() + + self._start_pause_btn = QPushButton("Start") + self._start_pause_btn.clicked.connect(self._on_start_pause_clicked) + btn_row.addWidget(self._start_pause_btn) + + cancel_btn = QPushButton("Cancel") + cancel_btn.setStyleSheet( + "QPushButton { background: #7f2020; color: white; border: none; " + "border-radius: 4px; padding: 6px 16px; }" + "QPushButton:hover { background: #9b2828; }" + ) + cancel_btn.clicked.connect(self.reject) + btn_row.addWidget(cancel_btn) + + root.addLayout(btn_row) + + # ------------------------------------------------------------------ + # Table helpers + # ------------------------------------------------------------------ + + def _fill_row(self, row: int, item: DownloadItem) -> None: + """Populate cells for a pre-allocated row (row must already exist in the table).""" + from PySide6.QtGui import QColor + self._table.setItem(row, _COL_MOD, QTableWidgetItem(item.mod_name)) + self._table.setItem(row, _COL_NAME, QTableWidgetItem(item.file_name)) + self._table.setItem(row, _COL_SIZE, QTableWidgetItem(_fmt_size(item.expected_size))) + colour = _STATUS_COLOURS.get(item.status, '#808080') + status_cell = QTableWidgetItem(_STATUS_LABELS.get(item.status, item.status)) + status_cell.setForeground(QColor(colour)) + if item.error_message: + status_cell.setToolTip(item.error_message) + self._table.setItem(row, _COL_STATUS, status_cell) + + def _update_row(self, row: int, item: DownloadItem) -> None: + from PySide6.QtGui import QColor + status_cell = self._table.item(row, _COL_STATUS) + if status_cell: + status_cell.setText(_STATUS_LABELS.get(item.status, item.status)) + status_cell.setForeground(QColor(_STATUS_COLOURS.get(item.status, '#808080'))) + status_cell.setToolTip(item.error_message or "") + + def _refresh_header(self) -> None: + items = self._manager.items + total = len(items) + complete = sum(1 for i in items if i.status == 'complete') + skipped = sum(1 for i in items if i.status == 'skipped') + remaining = total - complete - skipped + + pct = int(complete / total * 100) if total > 0 else 0 + self._progress_bar.setValue(pct) + self._progress_label.setText( + f"{complete} of {total} complete | {skipped} deferred | {remaining} remaining" + ) + self._retry_btn.setText(f"Retry Deferred ({skipped})") + self._retry_btn.setEnabled(skipped > 0) + + # ------------------------------------------------------------------ + # Slots + # ------------------------------------------------------------------ + + def _on_item_updated_slot(self, item: DownloadItem) -> None: + row = self._row_map.get(item.file_name) + if row is not None: + self._update_row(row, item) + self._refresh_header() + + def _on_concurrent_changed(self, value: int) -> None: + self._manager.set_concurrent_limit(value) + try: + cfg = ConfigHandler() + cfg.set("manual_download_concurrent_limit", int(value)) + cfg.save_config() + except Exception: + logger.debug("Could not persist manual_download_concurrent_limit", exc_info=True) + + def _on_pick_folder(self) -> None: + chosen = QFileDialog.getExistingDirectory(self, "Select watch folder", str(self._watch_dir)) + if chosen: + from jackify.backend.services.download_watcher_service import WatcherConfig + self._watch_dir = Path(chosen) + self._folder_label.setText(chosen) + self._manager._watch_dir = self._watch_dir + self._manager._watcher._config.watch_directory = self._watch_dir + self._manager._watcher._known = {} + try: + cfg = ConfigHandler() + cfg.set("manual_download_watch_directory", str(self._watch_dir)) + cfg.save_config() + except Exception: + logger.debug("Could not persist manual_download_watch_directory", exc_info=True) + + def _on_start_pause_clicked(self) -> None: + if not self._started: + self._started = True + self._paused = False + self._start_pause_btn.setText("Pause") + self._manager.resume() + return + + if not self._paused: + self._paused = True + self._start_pause_btn.setText("Resume") + self._manager.pause() + else: + self._paused = False + self._start_pause_btn.setText("Pause") + self._manager.resume() + + def _on_retry_skipped(self) -> None: + with self._manager._lock: + for item in self._manager._items: + if item.status in ('deferred', 'skipped'): + item.status = 'pending' + item.needs_user_retry = False + row = self._row_map.get(item.file_name) + if row is not None: + self._update_row(row, item) + self._manager._open_next_tabs() + self._refresh_header() + + def _on_defer_selected(self) -> None: + row = self._table.currentRow() + if row < 0: + return + file_item = self._table.item(row, _COL_NAME) + if file_item is None: + return + file_name = file_item.text().strip() + if not file_name: + return + self._manager.skip_item(file_name) + + def _on_open_selected(self) -> None: + row = self._table.currentRow() + if row < 0: + return + file_item = self._table.item(row, _COL_NAME) + if file_item is None: + return + file_name = file_item.text().strip() + if not file_name: + return + self._manager.reopen_item(file_name) + + def _on_row_double_clicked(self, row: int, _column: int) -> None: + file_item = self._table.item(row, _COL_NAME) + if file_item is None: + return + file_name = file_item.text() + if not file_name: + return + self._manager.reopen_item(file_name) + + def _on_all_done_slot(self, completed: int, skipped: int) -> None: + from PySide6.QtCore import QTimer + self._progress_label.setText( + f"All downloads complete ({completed} accepted, {skipped} deferred) — closing..." + ) + # Raise now while the dialog is still visible so the user sees the completion state + self._raise_main_window() + QTimer.singleShot(2000, self._close_and_refocus) + + def _close_and_refocus(self) -> None: + self.close() + # Closing a non-modal dialog can hand focus back to whatever was behind it + self._raise_main_window() + + def _raise_main_window(self) -> None: + try: + win = self.window() + if win: + win.raise_() + win.activateWindow() + except Exception: + pass + + def closeEvent(self, event) -> None: + # Don't stop the manager on close - install continues + event.accept() diff --git a/jackify/frontends/gui/dialogs/settings_dialog.py b/jackify/frontends/gui/dialogs/settings_dialog.py index 90d3f22..b070d00 100644 --- a/jackify/frontends/gui/dialogs/settings_dialog.py +++ b/jackify/frontends/gui/dialogs/settings_dialog.py @@ -274,13 +274,20 @@ class SettingsDialog(SettingsDialogTabsMixin, SettingsDialogProtonMixin, QDialog self.config_handler.set("proton_path", resolved_install_path) self.config_handler.set("proton_version", resolved_install_version) else: - # No Proton found - don't write anything, let engine auto-detect + # No Proton found - clear persisted selection so startup normalization + # can auto-heal once a compatible Proton is installed. logger.warning("Auto Proton selection failed: No Proton versions found") - # Don't modify existing config values + resolved_install_path = None + resolved_install_version = None + self.config_handler.set("proton_path", None) + self.config_handler.set("proton_version", None) except Exception as e: - # Exception during detection - log it and don't write anything + # Exception during detection - clear persisted selection to avoid stale path usage. logger.error(f"Auto Proton selection failed with exception: {e}", exc_info=True) - # Don't modify existing config values + resolved_install_path = None + resolved_install_version = None + self.config_handler.set("proton_path", None) + self.config_handler.set("proton_version", None) else: # User selected specific Proton version resolved_install_path = selected_install_proton_path @@ -392,4 +399,3 @@ class SettingsDialog(SettingsDialogTabsMixin, SettingsDialogProtonMixin, QDialog label = QLabel(text) label.setStyleSheet("font-weight: bold; color: #fff;") return label - diff --git a/jackify/frontends/gui/dialogs/success_dialog.py b/jackify/frontends/gui/dialogs/success_dialog.py index f36d34e..c50e1f7 100644 --- a/jackify/frontends/gui/dialogs/success_dialog.py +++ b/jackify/frontends/gui/dialogs/success_dialog.py @@ -86,6 +86,8 @@ class SuccessDialog(QDialog): modlist_name_html = f'{self.modlist_name}' if self.workflow_type == "install": suffix_text = "installed successfully!" + elif self.workflow_type == "update": + suffix_text = "updated successfully!" elif self.workflow_type == "configure_new": suffix_text = "configured successfully!" elif self.workflow_type == "configure_existing": @@ -220,6 +222,7 @@ class SuccessDialog(QDialog): """ workflow_messages = { "install": f"{self.modlist_name} installed successfully!", + "update": f"{self.modlist_name} updated successfully!", "configure_new": f"{self.modlist_name} configured successfully!", "configure_existing": f"{self.modlist_name} configuration updated successfully!", "tuxborn": f"Tuxborn installation completed successfully!", @@ -268,4 +271,4 @@ class SuccessDialog(QDialog): QApplication.quit() except Exception as e: logger.error(f"Error during safe exit: {e}") - QApplication.quit() \ No newline at end of file + QApplication.quit() diff --git a/jackify/frontends/gui/mixins/main_window_geometry.py b/jackify/frontends/gui/mixins/main_window_geometry.py index ad8d60a..5b49d90 100644 --- a/jackify/frontends/gui/mixins/main_window_geometry.py +++ b/jackify/frontends/gui/mixins/main_window_geometry.py @@ -44,6 +44,12 @@ class MainWindowGeometryMixin: def _is_compact_mode(self) -> bool: try: + if hasattr(self, 'wabbajack_installer_screen') and hasattr(self.wabbajack_installer_screen, 'show_details_checkbox'): + if self.wabbajack_installer_screen.show_details_checkbox.isChecked(): + return False + if hasattr(self, 'install_mo2_screen') and hasattr(self.install_mo2_screen, 'show_details_checkbox'): + if self.install_mo2_screen.show_details_checkbox.isChecked(): + return False if hasattr(self, 'install_modlist_screen') and hasattr(self.install_modlist_screen, 'show_details_checkbox'): if self.install_modlist_screen.show_details_checkbox.isChecked(): return False diff --git a/jackify/frontends/gui/mixins/main_window_ui.py b/jackify/frontends/gui/mixins/main_window_ui.py index 6f24791..1458414 100644 --- a/jackify/frontends/gui/mixins/main_window_ui.py +++ b/jackify/frontends/gui/mixins/main_window_ui.py @@ -220,6 +220,10 @@ class MainWindowUIMixin: stacked_widget=self.stacked_widget, additional_tasks_index=3, system_info=self.system_info ) self.install_mo2_screen = screen + try: + screen.resize_request.connect(self._on_child_resize_request) + except Exception: + pass return screen def _debug_screen_change(self, index): diff --git a/jackify/frontends/gui/screens/configure_existing_modlist.py b/jackify/frontends/gui/screens/configure_existing_modlist.py index ed03b4f..ed64f06 100644 --- a/jackify/frontends/gui/screens/configure_existing_modlist.py +++ b/jackify/frontends/gui/screens/configure_existing_modlist.py @@ -32,6 +32,7 @@ from .configure_existing_modlist_shortcuts import ConfigureExistingModlistShortc from .configure_existing_modlist_console import ConfigureExistingModlistConsoleMixin from .screen_back_mixin import ScreenBackMixin from .install_modlist_ttw import TTWIntegrationMixin +from .install_modlist_postinstall import PostInstallFeedbackMixin class ConfigureExistingModlistScreen( ScreenBackMixin, @@ -40,23 +41,35 @@ class ConfigureExistingModlistScreen( ConfigureExistingModlistWorkflowMixin, ConfigureExistingModlistShortcutsMixin, ConfigureExistingModlistConsoleMixin, + PostInstallFeedbackMixin, QWidget, ): resize_request = Signal(str) def cleanup_processes(self): """Clean up any running processes when the window closes or is cancelled""" - # Stop CPU tracking if active if hasattr(self, 'file_progress_list'): self.file_progress_list.stop_cpu_tracking() - # Clean up configuration thread if running - if hasattr(self, 'config_thread') and self.config_thread.isRunning(): - self.config_thread.terminate() - self.config_thread.wait(1000) + from PySide6.QtCore import QThread + for attr_name, value in list(vars(self).items()): + try: + if isinstance(value, QThread) and value.isRunning(): + try: + value.finished_signal.disconnect() + except Exception: + pass + value.terminate() + value.wait(2000) + setattr(self, attr_name, None) + except Exception: + pass def cancel_and_cleanup(self): """Handle Cancel button - clean up processes and go back""" + if getattr(self, '_vnv_controller', None) is not None: + self._vnv_controller.cleanup() + self._vnv_controller = None self.cleanup_processes() self.collapse_show_details_before_leave() self.go_back() @@ -65,16 +78,8 @@ class ConfigureExistingModlistScreen( """Called when the widget becomes visible - ensure collapsed state""" super().showEvent(event) - # Ensure initial collapsed layout first so UI is stable before async load try: - from PySide6.QtCore import Qt as _Qt - if self.show_details_checkbox.isChecked(): - self.show_details_checkbox.blockSignals(True) - self.show_details_checkbox.setChecked(False) - self.show_details_checkbox.blockSignals(False) - self._toggle_console_visibility(False) - - # Only set minimum size - DO NOT RESIZE + self.force_collapsed_details_state() main_window = self.window() if main_window: from PySide6.QtCore import QSize @@ -118,8 +123,15 @@ class ConfigureExistingModlistScreen( return # Check for VNV post-install automation after configuration - if install_dir: - self._check_and_run_vnv_automation(modlist_name, install_dir) + if install_dir and self._check_and_run_vnv_automation(modlist_name, install_dir): + self._pending_success_dialog_params = { + 'modlist_name': modlist_name, + 'workflow_type': 'configure_existing', + 'time_taken': self._calculate_time_taken(), + 'game_name': getattr(self, '_current_game_name', None), + 'enb_detected': enb_detected, + } + return # Calculate time taken time_taken = self._calculate_time_taken() @@ -202,10 +214,15 @@ class ConfigureExistingModlistScreen( # Re-enable controls (in case they were disabled from previous errors) self._enable_controls_after_operation() + self.force_collapsed_details_state() def cleanup(self): """Clean up any running threads when the screen is closed""" logger.debug("DEBUG: cleanup called - cleaning up ConfigurationThread") + + if getattr(self, '_vnv_controller', None) is not None: + self._vnv_controller.cleanup() + self._vnv_controller = None # Clean up config thread if running if hasattr(self, 'config_thread') and self.config_thread and self.config_thread.isRunning(): diff --git a/jackify/frontends/gui/screens/configure_existing_modlist_ui.py b/jackify/frontends/gui/screens/configure_existing_modlist_ui.py index b50567c..ac1c22f 100644 --- a/jackify/frontends/gui/screens/configure_existing_modlist_ui.py +++ b/jackify/frontends/gui/screens/configure_existing_modlist_ui.py @@ -51,6 +51,12 @@ class ConfigureExistingModlistUIMixin: self.progress_indicator = OverallProgressIndicator(show_progress_bar=True) self.progress_indicator.set_status("Ready to configure", 0) self.file_progress_list = FileProgressList() + self._post_install_sequence = self._build_post_install_sequence() + self._post_install_total_steps = len(self._post_install_sequence) + self._post_install_current_step = 0 + self._post_install_active = False + self._post_install_last_label = "" + self._bsa_hold_deadline = 0.0 # Create "Show Details" checkbox self.show_details_checkbox = QCheckBox("Show details") @@ -539,4 +545,3 @@ class ConfigureExistingModlistUIMixin: self.process_monitor.setPlainText('\n'.join(filtered)) except Exception as e: self.process_monitor.setPlainText(f"[process info unavailable: {e}]") - diff --git a/jackify/frontends/gui/screens/configure_existing_modlist_workflow.py b/jackify/frontends/gui/screens/configure_existing_modlist_workflow.py index ec45cf8..8bda5c7 100644 --- a/jackify/frontends/gui/screens/configure_existing_modlist_workflow.py +++ b/jackify/frontends/gui/screens/configure_existing_modlist_workflow.py @@ -1,10 +1,11 @@ """Workflow management for ConfigureExistingModlistScreen (Mixin).""" from PySide6.QtCore import QThread, Signal +from PySide6.QtWidgets import QMessageBox import os import time import logging from pathlib import Path -from typing import Optional + from jackify.shared.resolution_utils import get_resolution_fallback from jackify.shared.errors import configuration_failed @@ -188,7 +189,10 @@ class ConfigureExistingModlistWorkflowMixin: ) if not success: - self.error_occurred.emit("Configuration failed - check logs for details") + self.error_occurred.emit( + "Configuration did not complete successfully. " + "Review the latest workflow output above for the failing step." + ) except Exception as e: import traceback @@ -206,89 +210,64 @@ class ConfigureExistingModlistWorkflowMixin: self._safe_append_text(f"[ERROR] Failed to start configuration: {e}") MessageService.show_error(self, configuration_failed(str(e))) - def _check_and_run_vnv_automation(self, modlist_name: str, install_dir: str): - """Check if VNV automation should run and execute if applicable + def _check_and_run_vnv_automation(self, modlist_name: str, install_dir: str) -> bool: + """Check if VNV automation should run and start it if applicable. - Args: - modlist_name: Name of the installed modlist - install_dir: Installation directory path + Returns: + True if VNV automation is starting (caller should defer success dialog) + False if no VNV needed (show success dialog immediately) """ - try: - from pathlib import Path - 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.handlers.path_handler import PathHandler + from ..services.vnv_automation_controller import VNVAutomationController - # Get paths first (needed for VNV detection) - install_path = Path(install_dir) - - # Quick check before importing more (pass install location for ModOrganizer.ini check) - if not should_offer_vnv_automation(modlist_name, install_path): - return - game_paths = PathHandler().find_vanilla_game_paths() - game_root = game_paths.get('Fallout New Vegas') + self._vnv_controller = VNVAutomationController() + return self._vnv_controller.attempt( + parent=self, + modlist_name=modlist_name, + install_dir=install_dir, + on_progress=self._safe_append_text, + on_complete=self._on_vnv_complete, + begin_feedback=self._begin_post_install_feedback, + handle_feedback=self._handle_post_install_progress, + ) - if not game_root: - logger.debug("DEBUG: VNV automation skipped - FNV game root not found") - return - - # Confirmation callback - show dialog to user - def confirmation_callback(description: str) -> bool: - from ..services.message_service import MessageService - reply = MessageService.question( - self, - "VNV Post-Install Automation", - description, - critical=False, - safety_level="medium" - ) - return reply == QMessageBox.Yes - - # Manual file callback for non-Premium users - def manual_file_callback(title: str, instructions: str) -> Optional[Path]: - from PySide6.QtWidgets import QFileDialog - from ..services.message_service import MessageService - - # Show instructions - MessageService.information(self, title, instructions) - - # Open file picker - file_path, _ = QFileDialog.getOpenFileName( - self, - title, - str(Path.home() / "Downloads"), - "All Files (*.*)" - ) - - if file_path: - return Path(file_path).resolve() - return None - - # Run automation - automation_ran, error = run_vnv_automation_if_applicable( - modlist_name=modlist_name, - modlist_install_location=install_path, - game_root=game_root, - ttw_installer_path=AutomatedPrefixService.get_ttw_installer_path(), - progress_callback=None, # GUI doesn't need progress updates for post-install - manual_file_callback=manual_file_callback, - confirmation_callback=confirmation_callback + def _on_vnv_complete(self, success: bool, error: str): + """Handle VNV automation completion and show deferred success dialog.""" + self._end_post_install_feedback(not bool(error)) + if not success and error: + from ..services.message_service import MessageService + MessageService.warning( + self, + "VNV Automation Failed", + f"VNV post-install automation encountered an error:\n\n{error}\n\n" + "You can complete these steps manually by following the guide at:\n" + "https://vivanewvegas.moddinglinked.com/wabbajack.html" ) + elif success: + self._safe_append_text("VNV post-install automation completed successfully.") - if error: - from ..services.message_service import MessageService - MessageService.warning( - self, - "VNV Automation Failed", - f"VNV post-install automation encountered an error:\n\n{error}\n\n" - "You can complete these steps manually by following the guide at:\n" - "https://vivanewvegas.moddinglinked.com/wabbajack.html" - ) + if hasattr(self, '_pending_success_dialog_params'): + params = self._pending_success_dialog_params + del self._pending_success_dialog_params - except Exception as e: - logger.debug(f"ERROR: Failed to run VNV automation: {e}") - import traceback - logger.debug(f"Traceback: {traceback.format_exc()}") + self.file_progress_list.clear() + + from ..dialogs import SuccessDialog + success_dialog = SuccessDialog( + modlist_name=params['modlist_name'], + workflow_type=params['workflow_type'], + time_taken=params['time_taken'], + game_name=params['game_name'], + parent=self, + ) + success_dialog.show() + + if params.get('enb_detected'): + try: + from ..dialogs.enb_proton_dialog import ENBProtonDialog + enb_dialog = ENBProtonDialog(modlist_name=params['modlist_name'], parent=self) + enb_dialog.exec() + except Exception as e: + logger.warning("Failed to show ENB dialog: %s", e) def show_manual_steps_dialog(self, extra_warning=""): modlist_name = self.shortcut_combo.currentText().split('(')[0].strip() or "your modlist" @@ -372,4 +351,3 @@ class ConfigureExistingModlistWorkflowMixin: return f"{elapsed_minutes} minutes {elapsed_seconds_remainder} seconds" else: return f"{elapsed_seconds_remainder} seconds" - diff --git a/jackify/frontends/gui/screens/configure_new_modlist.py b/jackify/frontends/gui/screens/configure_new_modlist.py index d358200..735dd7c 100644 --- a/jackify/frontends/gui/screens/configure_new_modlist.py +++ b/jackify/frontends/gui/screens/configure_new_modlist.py @@ -35,14 +35,18 @@ from .configure_new_modlist_workflow import ConfigureNewModlistWorkflowMixin from .configure_new_modlist_dialogs import ConfigureNewModlistDialogsMixin, ModlistFetchThread, SelectionDialog from .screen_back_mixin import ScreenBackMixin from .install_modlist_ttw import TTWIntegrationMixin +from .install_modlist_postinstall import PostInstallFeedbackMixin logger = logging.getLogger(__name__) -class ConfigureNewModlistScreen(ScreenBackMixin, TTWIntegrationMixin, ConfigureNewModlistUISetupMixin, ConfigureNewModlistConsoleMixin, ConfigureNewModlistWorkflowMixin, ConfigureNewModlistDialogsMixin, QWidget): +class ConfigureNewModlistScreen(ScreenBackMixin, TTWIntegrationMixin, ConfigureNewModlistUISetupMixin, ConfigureNewModlistConsoleMixin, ConfigureNewModlistWorkflowMixin, ConfigureNewModlistDialogsMixin, PostInstallFeedbackMixin, QWidget): resize_request = Signal(str) def cancel_and_cleanup(self): """Handle Cancel button - clean up processes and go back""" + if getattr(self, '_vnv_controller', None) is not None: + self._vnv_controller.cleanup() + self._vnv_controller = None self.cleanup_processes() self.collapse_show_details_before_leave() self.go_back() @@ -50,23 +54,7 @@ class ConfigureNewModlistScreen(ScreenBackMixin, TTWIntegrationMixin, ConfigureN def showEvent(self, event): """Called when the widget becomes visible - ensure collapsed state""" super().showEvent(event) - - # Ensure initial collapsed layout each time this screen is opened - try: - from PySide6.QtCore import Qt as _Qt - # Ensure checkbox is unchecked without emitting signals - if self.show_details_checkbox.isChecked(): - self.show_details_checkbox.blockSignals(True) - self.show_details_checkbox.setChecked(False) - self.show_details_checkbox.blockSignals(False) - - # Force collapsed state - # Set console to hidden state without emitting signals - self.console.setVisible(False) - self.resize_request.emit("compact") - except Exception as e: - # If initial collapse fails, log but don't crash - print(f"Warning: Failed to set initial collapsed state: {e}") + self.force_collapsed_details_state() def on_configuration_complete(self, success, message, modlist_name, enb_detected=False): """Handle configuration completion (same as Tuxborn)""" @@ -88,8 +76,15 @@ class ConfigureNewModlistScreen(ScreenBackMixin, TTWIntegrationMixin, ConfigureN return # Check for VNV post-install automation after configuration - if install_dir: - self._check_and_run_vnv_automation(modlist_name, install_dir) + if install_dir and self._check_and_run_vnv_automation(modlist_name, install_dir): + self._pending_success_dialog_params = { + 'modlist_name': modlist_name, + 'workflow_type': 'configure_new', + 'time_taken': self._calculate_time_taken(), + 'game_name': getattr(self, '_current_game_name', None), + 'enb_detected': enb_detected, + } + return # Calculate time taken time_taken = self._calculate_time_taken() @@ -97,7 +92,6 @@ class ConfigureNewModlistScreen(ScreenBackMixin, TTWIntegrationMixin, ConfigureN # Clear Activity window before showing success dialog self.file_progress_list.clear() - # Show success dialog with celebration success_dialog = SuccessDialog( modlist_name=modlist_name, workflow_type="configure_new", @@ -106,16 +100,14 @@ class ConfigureNewModlistScreen(ScreenBackMixin, TTWIntegrationMixin, ConfigureN parent=self ) success_dialog.show() - - # Show ENB Proton dialog if ENB was detected (use stored detection result, no re-detection) + if enb_detected: try: from ..dialogs.enb_proton_dialog import ENBProtonDialog enb_dialog = ENBProtonDialog(modlist_name=modlist_name, parent=self) - enb_dialog.exec() # Modal dialog - blocks until user clicks OK + enb_dialog.exec() except Exception as e: - # Non-blocking: if dialog fails, just log and continue - logger.warning(f"Failed to show ENB dialog: {e}") + logger.warning("Failed to show ENB dialog: %s", e) else: self._safe_append_text(f"Configuration failed: {message}") MessageService.show_error(self, configuration_failed(str(message))) @@ -169,10 +161,15 @@ class ConfigureNewModlistScreen(ScreenBackMixin, TTWIntegrationMixin, ConfigureN # Re-enable controls (in case they were disabled from previous errors) self._enable_controls_after_operation() + self.force_collapsed_details_state() def cleanup(self): """Clean up any running threads when the screen is closed""" logger.debug("DEBUG: cleanup called - cleaning up threads") + + if getattr(self, '_vnv_controller', None) is not None: + self._vnv_controller.cleanup() + self._vnv_controller = None # Clean up automated prefix thread if running if hasattr(self, 'automated_prefix_thread') and self.automated_prefix_thread and self.automated_prefix_thread.isRunning(): diff --git a/jackify/frontends/gui/screens/configure_new_modlist_console.py b/jackify/frontends/gui/screens/configure_new_modlist_console.py index a5d0ac3..a02c6e4 100644 --- a/jackify/frontends/gui/screens/configure_new_modlist_console.py +++ b/jackify/frontends/gui/screens/configure_new_modlist_console.py @@ -3,13 +3,14 @@ import os import re import time -from PySide6.QtCore import QTimer +from PySide6.QtCore import QTimer, Qt from PySide6.QtWidgets import QFileDialog from jackify.shared.progress_models import FileProgress, OperationType +from .screen_focus_reclaim import FocusReclaimMixin, STEAM_RESTART_SENTINEL -class ConfigureNewModlistConsoleMixin: +class ConfigureNewModlistConsoleMixin(FocusReclaimMixin): """Mixin providing console output management for ConfigureNewModlistScreen.""" def _handle_progress_update(self, text): @@ -26,10 +27,12 @@ class ConfigureNewModlistConsoleMixin: self._stop_component_install_pulse() self.progress_indicator.set_status("Restarting Steam...", 20) self.file_progress_list.update_or_add_item("__phase__", "Restarting Steam...", 0.0) - elif "steam restart" in message_lower and "success" in message_lower: + elif "steam started successfully" in message_lower or ("steam restart" in message_lower and "success" in message_lower): self._stop_component_install_pulse() self.progress_indicator.set_status("Steam restarted successfully", 30) self.file_progress_list.update_or_add_item("__phase__", "Steam restarted", 0.0) + elif STEAM_RESTART_SENTINEL in text: + self._start_focus_reclaim_retries() elif "creating proton prefix" in message_lower or "prefix creation" in message_lower: self._stop_component_install_pulse() self.progress_indicator.set_status("Creating Proton prefix...", 50) @@ -169,4 +172,3 @@ class ConfigureNewModlistConsoleMixin: if file: self.install_dir_edit.setText(os.path.realpath(file)) - diff --git a/jackify/frontends/gui/screens/configure_new_modlist_dialogs.py b/jackify/frontends/gui/screens/configure_new_modlist_dialogs.py index 01a0035..a0908bf 100644 --- a/jackify/frontends/gui/screens/configure_new_modlist_dialogs.py +++ b/jackify/frontends/gui/screens/configure_new_modlist_dialogs.py @@ -1,10 +1,12 @@ """Dialog management for ConfigureNewModlistScreen (Mixin).""" +import os from PySide6.QtWidgets import QDialog, QVBoxLayout, QLabel, QLineEdit, QPushButton, QHBoxLayout, QFileDialog, QMessageBox, QApplication, QListWidget, QListWidgetItem from PySide6.QtCore import Qt, QThread, Signal from PySide6.QtGui import QTextCursor from pathlib import Path -from typing import Optional + import subprocess +from jackify.frontends.gui.dialogs.existing_setup_dialog import prompt_existing_setup_dialog from jackify.frontends.gui.services.message_service import MessageService from jackify.shared.errors import manual_steps_incomplete import logging @@ -75,131 +77,85 @@ class SelectionDialog(QDialog): class ConfigureNewModlistDialogsMixin: """Mixin providing dialog management for ConfigureNewModlistScreen.""" + def _restore_controls_after_shortcut_dialog_abort(self): + """Return Configure New to an editable state when shortcut resolution is aborted.""" + try: + self._enable_controls_after_operation() + except Exception: + pass + def cleanup_processes(self): """Clean up any running processes when the window closes or is cancelled""" - # Stop CPU tracking if active if hasattr(self, 'file_progress_list'): self.file_progress_list.stop_cpu_tracking() - # Clean up automated prefix thread if running - if hasattr(self, 'automated_prefix_thread') and self.automated_prefix_thread.isRunning(): - self.automated_prefix_thread.terminate() - self.automated_prefix_thread.wait(1000) - - # Clean up configuration thread if running - if hasattr(self, 'config_thread') and self.config_thread.isRunning(): - self.config_thread.terminate() - self.config_thread.wait(1000) + from PySide6.QtCore import QThread + for attr_name, value in list(vars(self).items()): + try: + if isinstance(value, QThread) and value.isRunning(): + value.terminate() + value.wait(2000) + setattr(self, attr_name, None) + except Exception: + pass def show_shortcut_conflict_dialog(self, conflicts): - """Show dialog to resolve shortcut name conflicts""" + """Show dialog to reuse an existing shortcut or choose a new name.""" conflict_names = [c['name'] for c in conflicts] - conflict_info = f"Found existing Steam shortcut: '{conflict_names[0]}'" - + existing_name = conflict_names[0] + modlist_name = self.modlist_name_edit.text().strip() - - # Create dialog with Jackify styling - from PySide6.QtWidgets import QDialog, QVBoxLayout, QLabel, QLineEdit, QPushButton, QHBoxLayout - from PySide6.QtCore import Qt - - dialog = QDialog(self) - dialog.setWindowTitle("Steam Shortcut Conflict") - dialog.setModal(True) - dialog.resize(450, 180) - - # Apply Jackify dark theme styling - dialog.setStyleSheet(""" - QDialog { - background-color: #2b2b2b; - color: #ffffff; - } - QLabel { - color: #ffffff; - font-size: 14px; - padding: 10px 0px; - } - QLineEdit { - background-color: #404040; - color: #ffffff; - border: 2px solid #555555; - border-radius: 4px; - padding: 8px; - font-size: 14px; - selection-background-color: #3fd0ea; - } - QLineEdit:focus { - border-color: #3fd0ea; - } - QPushButton { - background-color: #404040; - color: #ffffff; - border: 2px solid #555555; - border-radius: 4px; - padding: 8px 16px; - font-size: 14px; - min-width: 120px; - } - QPushButton:hover { - background-color: #505050; - border-color: #3fd0ea; - } - QPushButton:pressed { - background-color: #303030; - } - """) - - layout = QVBoxLayout(dialog) - layout.setContentsMargins(20, 20, 20, 20) - layout.setSpacing(15) - - # Conflict message - conflict_label = QLabel(f"{conflict_info}\n\nPlease choose a different name for your shortcut:") - layout.addWidget(conflict_label) - - # Text input for new name - name_input = QLineEdit(modlist_name) - name_input.selectAll() - layout.addWidget(name_input) - - # Buttons - button_layout = QHBoxLayout() - button_layout.setSpacing(10) - - create_button = QPushButton("Create with New Name") - cancel_button = QPushButton("Cancel") - - button_layout.addStretch() - button_layout.addWidget(cancel_button) - button_layout.addWidget(create_button) - layout.addLayout(button_layout) - + install_dir = os.path.dirname(self.install_dir_edit.text().strip()) if self.install_dir_edit.text().strip().endswith('ModOrganizer.exe') else self.install_dir_edit.text().strip() + + action, new_name = prompt_existing_setup_dialog( + self, + window_title="Existing Modlist Setup Detected", + heading="Modlist Update or New Install", + body=( + "Jackify detected an existing Steam shortcut for this setup.\n\n" + "If you are updating an existing modlist or reconfiguring it, choose " + "'Use Existing Setup'. If you want a separate Steam entry, enter a different " + "name and choose 'Create New Shortcut'." + ), + existing_name=existing_name, + requested_name=modlist_name, + install_dir=install_dir, + field_label="New shortcut name", + reuse_label="Use Existing Setup", + new_label="Create New Shortcut", + cancel_label="Cancel", + ) + # Connect signals - def on_create(): - new_name = name_input.text().strip() + if action == "new": if new_name and new_name != modlist_name: - dialog.accept() - # Retry workflow with new name self.retry_automated_workflow_with_new_name(new_name) elif new_name == modlist_name: - # Same name - show warning - from jackify.backend.services.message_service import MessageService MessageService.warning(self, "Same Name", "Please enter a different name to resolve the conflict.") + self._restore_controls_after_shortcut_dialog_abort() else: - # Empty name - from jackify.backend.services.message_service import MessageService MessageService.warning(self, "Invalid Name", "Please enter a valid shortcut name.") - - def on_cancel(): - dialog.reject() + self._restore_controls_after_shortcut_dialog_abort() + elif action == "reuse": + existing_appid = conflicts[0].get('appid') + if not existing_appid: + MessageService.warning( + self, + "Existing Setup Not Found", + "Jackify could not determine the Steam AppID for the existing shortcut.", + ) + self._restore_controls_after_shortcut_dialog_abort() + return + self._safe_append_text(f"Reusing existing Steam shortcut '{existing_name}'.") + self.continue_configuration_after_automated_prefix( + str(existing_appid), + existing_name, + install_dir, + None, + ) + else: self._safe_append_text("Shortcut creation cancelled by user") - - create_button.clicked.connect(on_create) - cancel_button.clicked.connect(on_cancel) - - # Make Enter key work - name_input.returnPressed.connect(on_create) - - dialog.exec() + self._restore_controls_after_shortcut_dialog_abort() def retry_automated_workflow_with_new_name(self, new_name): """Retry the automated workflow with a new shortcut name""" @@ -228,89 +184,64 @@ class ConfigureNewModlistDialogsMixin: MessageService.show_error(self, manual_steps_incomplete()) self.on_configuration_complete(False, "Manual steps validation failed after multiple attempts", self.modlist_name_edit.text().strip()) - def _check_and_run_vnv_automation(self, modlist_name: str, install_dir: str): - """Check if VNV automation should run and execute if applicable + def _check_and_run_vnv_automation(self, modlist_name: str, install_dir: str) -> bool: + """Check if VNV automation should run and start it if applicable. - Args: - modlist_name: Name of the installed modlist - install_dir: Installation directory path + Returns: + True if VNV automation is starting (caller should defer success dialog) + False if no VNV needed (show success dialog immediately) """ - try: - from pathlib import Path - 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.handlers.path_handler import PathHandler + from ..services.vnv_automation_controller import VNVAutomationController - # Get paths first (needed for VNV detection) - install_path = Path(install_dir) - - # Quick check before importing more (pass install location for ModOrganizer.ini check) - if not should_offer_vnv_automation(modlist_name, install_path): - return - game_paths = PathHandler().find_vanilla_game_paths() - game_root = game_paths.get('Fallout New Vegas') + self._vnv_controller = VNVAutomationController() + return self._vnv_controller.attempt( + parent=self, + modlist_name=modlist_name, + install_dir=install_dir, + on_progress=self._safe_append_text, + on_complete=self._on_vnv_complete, + begin_feedback=self._begin_post_install_feedback, + handle_feedback=self._handle_post_install_progress, + ) - if not game_root: - logger.debug("DEBUG: VNV automation skipped - FNV game root not found") - return - - # Confirmation callback - show dialog to user - def confirmation_callback(description: str) -> bool: - from ..services.message_service import MessageService - reply = MessageService.question( - self, - "VNV Post-Install Automation", - description, - critical=False, - safety_level="medium" - ) - return reply == QMessageBox.Yes - - # Manual file callback for non-Premium users - def manual_file_callback(title: str, instructions: str) -> Optional[Path]: - from PySide6.QtWidgets import QFileDialog - from ..services.message_service import MessageService - - # Show instructions - MessageService.information(self, title, instructions) - - # Open file picker - file_path, _ = QFileDialog.getOpenFileName( - self, - title, - str(Path.home() / "Downloads"), - "All Files (*.*)" - ) - - if file_path: - return Path(file_path).resolve() - return None - - # Run automation - automation_ran, error = run_vnv_automation_if_applicable( - modlist_name=modlist_name, - modlist_install_location=install_path, - game_root=game_root, - ttw_installer_path=AutomatedPrefixService.get_ttw_installer_path(), - progress_callback=None, # GUI doesn't need progress updates for post-install - manual_file_callback=manual_file_callback, - confirmation_callback=confirmation_callback + def _on_vnv_complete(self, success: bool, error: str): + """Handle VNV automation completion and show deferred success dialog.""" + self._end_post_install_feedback(not bool(error)) + if not success and error: + from ..services.message_service import MessageService + MessageService.warning( + self, + "VNV Automation Failed", + f"VNV post-install automation encountered an error:\n\n{error}\n\n" + "You can complete these steps manually by following the guide at:\n" + "https://vivanewvegas.moddinglinked.com/wabbajack.html" ) + elif success: + self._safe_append_text("VNV post-install automation completed successfully.") - if error: - from ..services.message_service import MessageService - MessageService.warning( - self, - "VNV Automation Failed", - f"VNV post-install automation encountered an error:\n\n{error}\n\n" - "You can complete these steps manually by following the guide at:\n" - "https://vivanewvegas.moddinglinked.com/wabbajack.html" - ) + if hasattr(self, '_pending_success_dialog_params'): + params = self._pending_success_dialog_params + del self._pending_success_dialog_params - except Exception as e: - logger.debug(f"ERROR: Failed to run VNV automation: {e}") - import traceback - logger.debug(f"Traceback: {traceback.format_exc()}") + self.file_progress_list.clear() + + from ..dialogs import SuccessDialog + success_dialog = SuccessDialog( + modlist_name=params['modlist_name'], + workflow_type=params['workflow_type'], + time_taken=params['time_taken'], + game_name=params['game_name'], + parent=self, + ) + success_dialog.show() + + if params.get('enb_detected'): + try: + from ..dialogs.enb_proton_dialog import ENBProtonDialog + enb_dialog = ENBProtonDialog(modlist_name=params['modlist_name'], parent=self) + enb_dialog.exec() + except Exception as e: + logger.warning("Failed to show ENB dialog: %s", e) def show_next_steps_dialog(self, message): dlg = QDialog(self) @@ -335,4 +266,3 @@ class ConfigureNewModlistDialogsMixin: btn_return.clicked.connect(on_return) btn_exit.clicked.connect(on_exit) dlg.exec() - diff --git a/jackify/frontends/gui/screens/configure_new_modlist_ui_setup.py b/jackify/frontends/gui/screens/configure_new_modlist_ui_setup.py index d931ce1..0fba309 100644 --- a/jackify/frontends/gui/screens/configure_new_modlist_ui_setup.py +++ b/jackify/frontends/gui/screens/configure_new_modlist_ui_setup.py @@ -55,6 +55,12 @@ class ConfigureNewModlistUISetupMixin: self.progress_indicator = OverallProgressIndicator(show_progress_bar=True) self.progress_indicator.set_status("Ready to configure", 0) self.file_progress_list = FileProgressList() + self._post_install_sequence = self._build_post_install_sequence() + self._post_install_total_steps = len(self._post_install_sequence) + self._post_install_current_step = 0 + self._post_install_active = False + self._post_install_last_label = "" + self._bsa_hold_deadline = 0.0 # Create "Show Details" checkbox self.show_details_checkbox = QCheckBox("Show details") @@ -601,4 +607,3 @@ class ConfigureNewModlistUISetupMixin: f"Unable to verify protontricks installation: {e}\n\n" "Continuing anyway, but some features may not work correctly.") return True # Continue anyway - diff --git a/jackify/frontends/gui/screens/configure_new_modlist_workflow.py b/jackify/frontends/gui/screens/configure_new_modlist_workflow.py index b08d586..c75918d 100644 --- a/jackify/frontends/gui/screens/configure_new_modlist_workflow.py +++ b/jackify/frontends/gui/screens/configure_new_modlist_workflow.py @@ -145,7 +145,6 @@ class ConfigureNewModlistWorkflowMixin: progress_update = Signal(str) workflow_complete = Signal(object) # Will emit the result tuple error_occurred = Signal(object) # error (JackifyError or str) - def __init__(self, modlist_name, install_dir, mo2_exe_path, steamdeck, auto_restart): super().__init__() self.modlist_name = modlist_name @@ -153,27 +152,23 @@ class ConfigureNewModlistWorkflowMixin: self.mo2_exe_path = mo2_exe_path self.steamdeck = steamdeck self.auto_restart = auto_restart - + def run(self): try: from jackify.backend.services.automated_prefix_service import AutomatedPrefixService - - # Initialize the automated prefix service + prefix_service = AutomatedPrefixService() - - # Define progress callback for GUI updates + def progress_callback(message): self.progress_update.emit(message) - - # Run the automated workflow (this contains the blocking operations) + result = prefix_service.run_working_workflow( - self.modlist_name, self.install_dir, self.mo2_exe_path, + self.modlist_name, self.install_dir, self.mo2_exe_path, progress_callback, steamdeck=self.steamdeck, auto_restart=self.auto_restart ) - - # Emit the result + self.workflow_complete.emit(result) - + except Exception as e: from jackify.shared.errors import JackifyError, prefix_creation_failed if not isinstance(e, JackifyError): @@ -474,7 +469,10 @@ class ConfigureNewModlistWorkflowMixin: ) if not success: - self.error_occurred.emit("Configuration failed - check logs for details") + self.error_occurred.emit( + "Configuration did not complete successfully. " + "Review the latest workflow output above for the failing step." + ) except Exception as e: import traceback @@ -509,4 +507,3 @@ class ConfigureNewModlistWorkflowMixin: return f"{elapsed_minutes} minutes {elapsed_seconds_remainder} seconds" else: return f"{elapsed_seconds_remainder} seconds" - diff --git a/jackify/frontends/gui/screens/install_mo2_screen.py b/jackify/frontends/gui/screens/install_mo2_screen.py index 014e080..c4da4da 100644 --- a/jackify/frontends/gui/screens/install_mo2_screen.py +++ b/jackify/frontends/gui/screens/install_mo2_screen.py @@ -7,6 +7,7 @@ MO2SetupService. No Wabbajack modlist required. import logging import os +from datetime import datetime from pathlib import Path from typing import Optional @@ -18,11 +19,14 @@ from PySide6.QtWidgets import ( from PySide6.QtCore import Qt, QThread, Signal, QSize from jackify.backend.models.configuration import SystemInfo +from jackify.backend.services.automated_prefix_service import AutomatedPrefixService from jackify.shared.errors import mo2_setup_failed from jackify.shared.progress_models import FileProgress, OperationType +from ..dialogs.existing_setup_dialog import prompt_existing_setup_dialog from ..services.message_service import MessageService from ..shared_theme import JACKIFY_COLOR_BLUE, DEBUG_BORDERS from ..utils import set_responsive_minimum +from .screen_focus_reclaim import FocusReclaimMixin, STEAM_RESTART_SENTINEL from ..widgets.progress_indicator import OverallProgressIndicator from ..widgets.file_progress_list import FileProgressList from .screen_back_mixin import ScreenBackMixin @@ -37,10 +41,11 @@ class MO2SetupWorker(QThread): log_output = Signal(str) setup_complete = Signal(bool, object, str) # success, app_id (int|None), error_msg - def __init__(self, install_dir: Path, shortcut_name: str): + def __init__(self, install_dir: Path, shortcut_name: str, existing_appid: int | None = None): super().__init__() self.install_dir = install_dir self.shortcut_name = shortcut_name + self.existing_appid = existing_appid def run(self): from jackify.backend.services.mo2_setup_service import MO2SetupService @@ -56,6 +61,7 @@ class MO2SetupWorker(QThread): success, app_id, error_msg = service.setup_mo2( install_dir=self.install_dir, shortcut_name=self.shortcut_name, + existing_appid=self.existing_appid, progress_callback=_progress, should_cancel=self.isInterruptionRequested, ) @@ -68,7 +74,7 @@ class MO2SetupWorker(QThread): self.setup_complete.emit(False, None, str(e)) -class InstallMO2Screen(ScreenBackMixin, QWidget): +class InstallMO2Screen(ScreenBackMixin, FocusReclaimMixin, QWidget): """Standalone MO2 setup screen""" resize_request = Signal(str) @@ -90,6 +96,10 @@ class InstallMO2Screen(ScreenBackMixin, QWidget): self._user_manually_scrolled = False self._was_at_bottom = True + from jackify.shared.paths import get_jackify_logs_dir + self.log_path = get_jackify_logs_dir() / "MO2_Install_workflow.log" + os.makedirs(os.path.dirname(self.log_path), exist_ok=True) + self.progress_indicator = OverallProgressIndicator(show_progress_bar=False) self.progress_indicator.set_status("Ready", 0) @@ -281,7 +291,16 @@ class InstallMO2Screen(ScreenBackMixin, QWidget): def _on_show_details_toggled(self, checked): self.console.setVisible(checked) - self.resize_request.emit("expand" if checked else "collapse") + if checked: + self.console.setMinimumHeight(200) + self.console.setMaximumHeight(16777215) + self.console.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + self.resize_request.emit("expand") + else: + self.console.setMinimumHeight(0) + self.console.setMaximumHeight(0) + self.console.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Fixed) + self.resize_request.emit("compact") def _browse_folder(self): folder = QFileDialog.getExistingDirectory( @@ -339,15 +358,75 @@ class InstallMO2Screen(ScreenBackMixin, QWidget): if confirm != QMessageBox.Yes: return + existing_appid = None + candidate_exe = install_dir / "ModOrganizer.exe" + prefix_service = AutomatedPrefixService() + conflict_result = prefix_service.handle_existing_shortcut_conflict( + shortcut_name, + str(candidate_exe), + str(install_dir), + ) + if isinstance(conflict_result, list): + action, new_name = prompt_existing_setup_dialog( + self, + window_title="Existing Modlist Setup Detected", + heading="Use Existing Setup or Create a New Shortcut", + body=( + "Jackify found an existing Steam shortcut for this Mod Organizer 2 setup.\n\n" + "Choose 'Use Existing Setup' to reuse the current Steam shortcut, or enter a " + "different name to create a separate shortcut." + ), + existing_name=conflict_result[0].get("name", shortcut_name), + requested_name=shortcut_name, + install_dir=str(install_dir), + field_label="New shortcut name", + reuse_label="Use Existing Setup", + new_label="Create New Shortcut", + cancel_label="Cancel", + ) + if action == "reuse": + existing_appid = conflict_result[0].get("appid") + if not existing_appid: + MessageService.warning(self, "Existing Setup Not Found", "Jackify could not determine the Steam AppID for the existing shortcut.") + return + self.console.append(f"Reusing existing Steam shortcut '{shortcut_name}'.") + elif action == "new": + if not new_name: + MessageService.warning(self, "Invalid Name", "Please enter a valid shortcut name.") + return + if new_name == shortcut_name: + MessageService.warning(self, "Same Name", "Please enter a different name to create a separate shortcut.") + return + shortcut_name = new_name + self.shortcut_name_edit.setText(new_name) + else: + self.console.append("Shortcut creation cancelled by user") + return + self.console.clear() self.file_progress_list.clear() self.file_progress_list.start_cpu_tracking() + from jackify.backend.handlers.logging_handler import LoggingHandler + log_handler = LoggingHandler() + log_handler.rotate_log_file_per_run(self.log_path, backup_count=5) + + self._write_to_log_file("=" * 60) + self._write_to_log_file("MO2 Setup Started") + self._write_to_log_file(f"Install directory: {install_dir}") + self._write_to_log_file(f"Shortcut name: {shortcut_name}") + if existing_appid: + self._write_to_log_file(f"Existing AppID: {existing_appid}") + self._write_to_log_file("=" * 60) + self.start_btn.setEnabled(False) - self.cancel_btn.setEnabled(False) + self.cancel_btn.setEnabled(True) + self.cancel_btn.setText("Cancel Setup") + self.shortcut_name_edit.setEnabled(False) + self.install_dir_edit.setEnabled(False) self.progress_indicator.set_status("Starting...", 0) - self.worker = MO2SetupWorker(install_dir, shortcut_name) + self.worker = MO2SetupWorker(install_dir, shortcut_name, int(existing_appid) if existing_appid else None) self.worker.progress_update.connect(self._on_progress_update) self.worker.progress_update.connect(self._on_activity_progress) self.worker.log_output.connect(self._on_log_output) @@ -356,14 +435,25 @@ class InstallMO2Screen(ScreenBackMixin, QWidget): def _on_progress_update(self, message: str): self.progress_indicator.set_status(message, 0) + if STEAM_RESTART_SENTINEL in message: + self._start_focus_reclaim_retries() def _on_log_output(self, message: str): + self._write_to_log_file(message) scrollbar = self.console.verticalScrollBar() was_at_bottom = scrollbar.value() >= scrollbar.maximum() - 1 self.console.append(message) if was_at_bottom and not self._user_manually_scrolled: scrollbar.setValue(scrollbar.maximum()) + def _write_to_log_file(self, message: str): + try: + timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + with open(self.log_path, 'a', encoding='utf-8') as f: + f.write(f"[{timestamp}] {message}\n") + except Exception: + pass + def _on_setup_complete(self, success: bool, app_id, error_msg: str): self.file_progress_list.stop_cpu_tracking() @@ -384,6 +474,9 @@ class InstallMO2Screen(ScreenBackMixin, QWidget): self.start_btn.setEnabled(True) self.cancel_btn.setEnabled(True) + self.cancel_btn.setText("Cancel") + self.shortcut_name_edit.setEnabled(True) + self.install_dir_edit.setEnabled(True) if self.worker is not None: try: self.worker.deleteLater() @@ -429,22 +522,11 @@ class InstallMO2Screen(ScreenBackMixin, QWidget): self.file_progress_list.clear() self.console.clear() self.progress_indicator.set_status("Ready", 0) - if self.show_details_checkbox.isChecked(): - self.show_details_checkbox.blockSignals(True) - self.show_details_checkbox.setChecked(False) - self.show_details_checkbox.blockSignals(False) - self.console.setVisible(False) - self.resize_request.emit("collapse") + self.force_collapsed_details_state() def showEvent(self, event): super().showEvent(event) - # Keep MO2 screen consistent with other workflows: details collapsed by default. - if self.show_details_checkbox.isChecked(): - self.show_details_checkbox.blockSignals(True) - self.show_details_checkbox.setChecked(False) - self.show_details_checkbox.blockSignals(False) - self.console.setVisible(False) - self.resize_request.emit("collapse") + self.force_collapsed_details_state() try: main_window = self.window() if main_window: diff --git a/jackify/frontends/gui/screens/install_modlist.py b/jackify/frontends/gui/screens/install_modlist.py index ff73fbc..5d48eee 100644 --- a/jackify/frontends/gui/screens/install_modlist.py +++ b/jackify/frontends/gui/screens/install_modlist.py @@ -415,6 +415,10 @@ class InstallModlistScreen(ScreenBackMixin, InstallModlistUISetupMixin, ConsoleO """Clean up any running processes when the window closes or is cancelled""" logger.debug("DEBUG: cleanup_processes called - cleaning up InstallationThread and other processes") + if getattr(self, '_vnv_controller', None) is not None: + self._vnv_controller.cleanup() + self._vnv_controller = None + def _stop_thread(attr_name: str, cancel_method: Optional[str] = None, cooperative_ms: int = 5000, force_ms: int = 10000): thread = getattr(self, attr_name, None) if thread is None: @@ -470,12 +474,19 @@ class InstallModlistScreen(ScreenBackMixin, InstallModlistUISetupMixin, ConsoleO pass setattr(self, attr_name, None) - # Always stop installer thread first; this is the most likely source of QThread teardown aborts. + # Always stop installer thread first; it needs cancel() not terminate(). _stop_thread('install_thread', cancel_method='cancel', cooperative_ms=15000, force_ms=10000) - # Stop remaining worker threads. - for thread_name in ('prefix_thread', 'config_thread', 'fetch_thread', '_gallery_cache_preload_thread'): - _stop_thread(thread_name) + # Stop any remaining QThread instances on this object, regardless of attribute name. + from PySide6.QtCore import QThread + for attr_name, value in list(vars(self).items()): + if attr_name == 'install_thread': + continue + try: + if isinstance(value, QThread): + _stop_thread(attr_name) + except Exception: + pass def cancel_installation(self): """Cancel the currently running installation""" @@ -499,6 +510,29 @@ class InstallModlistScreen(ScreenBackMixin, InstallModlistUISetupMixin, ConsoleO if hasattr(self, 'progress_indicator'): self.progress_indicator.set_status("Cancelled", None) + # Stop manual download manager and close dialog if active + if getattr(self, '_manual_dl_manager', None) is not None: + try: + self._manual_dl_manager.stop() + except Exception: + pass + self._manual_dl_manager = None + if getattr(self, '_manual_dl_dialog', None) is not None: + try: + self._manual_dl_dialog.close() + except Exception: + pass + self._manual_dl_dialog = None + if getattr(self, '_non_premium_info_dlg', None) is not None: + try: + self._non_premium_info_dlg.close() + except Exception: + pass + self._non_premium_info_dlg = None + self._non_premium_gate_enabled = False + self._non_premium_info_acknowledged = False + self._pending_manual_download_events = None + # Cancel the installation thread if it exists if hasattr(self, 'install_thread') and self.install_thread and self.install_thread.isRunning(): self.install_thread.cancel() diff --git a/jackify/frontends/gui/screens/install_modlist_configuration.py b/jackify/frontends/gui/screens/install_modlist_configuration.py index 26547aa..4770293 100644 --- a/jackify/frontends/gui/screens/install_modlist_configuration.py +++ b/jackify/frontends/gui/screens/install_modlist_configuration.py @@ -1,6 +1,7 @@ """Configuration phase workflow for InstallModlistScreen (Mixin).""" from PySide6.QtWidgets import QMessageBox, QProgressDialog -from PySide6.QtCore import Qt, QThread, Signal +from PySide6.QtCore import Qt, QThread, Signal, QTimer +from .screen_focus_reclaim import FocusReclaimMixin, STEAM_RESTART_SENTINEL from PySide6.QtGui import QFont from jackify.frontends.gui.services.message_service import MessageService from jackify.shared.errors import manual_steps_incomplete, configuration_failed @@ -16,7 +17,7 @@ import logging logger = logging.getLogger(__name__) from .install_modlist_shortcut_dialog import InstallModlistShortcutDialogMixin -class ConfigurationPhaseMixin(InstallModlistShortcutDialogMixin): +class ConfigurationPhaseMixin(FocusReclaimMixin, InstallModlistShortcutDialogMixin): """Mixin providing configuration phase workflow and dialog management for InstallModlistScreen.""" def on_configuration_progress(self, progress_msg): @@ -43,14 +44,9 @@ class ConfigurationPhaseMixin(InstallModlistShortcutDialogMixin): pass finally: self.steam_restart_progress = None - # Controls are managed by the proper control management system - # Delay focus reclaim so Steam's window finishes painting before we steal it back - try: - from PySide6.QtCore import QTimer - win = self.window() - QTimer.singleShot(10000, lambda: (win.raise_(), win.activateWindow())) - except Exception: - pass + # Controls are managed by the proper control management system. + # Reclaim focus with bounded retries because Steam restart timing varies. + self._start_focus_reclaim_retries() def _detect_game_type_from_mo2_ini(self, install_dir: str) -> str: """Detect game type by checking ModOrganizer.ini for loader executables.""" @@ -167,13 +163,20 @@ class ConfigurationPhaseMixin(InstallModlistShortcutDialogMixin): # No VNV automation - end post-install feedback now self._end_post_install_feedback(True) + if getattr(self, "_is_update_install", False): + try: + self._verify_update_ini_after_configuration(install_dir) + except Exception as e: + logger.warning("Update mode verify: failed post-config INI verification: %s", e) + # Clear Activity window before showing success dialog self.file_progress_list.clear() # Show normal success dialog + workflow_type = "update" if getattr(self, "_is_update_install", False) else "install" success_dialog = SuccessDialog( modlist_name=modlist_name, - workflow_type="install", + workflow_type=workflow_type, time_taken=time_str, game_name=game_name, parent=self @@ -196,7 +199,19 @@ class ConfigurationPhaseMixin(InstallModlistShortcutDialogMixin): else: # Configuration failed for other reasons self._end_post_install_feedback(False) - MessageService.show_error(self, configuration_failed("Post-install configuration failed.")) + MessageService.show_error( + self, + configuration_failed( + "Post-install configuration failed.", + context={ + "operation": "install_modlist", + "step": "post_install_configuration", + "modlist_name": modlist_name, + "install_dir": install_dir, + "workflow_type": "update" if getattr(self, "_is_update_install", False) else "install", + }, + ), + ) except Exception as e: # Ensure controls are re-enabled even on unexpected errors self._enable_controls_after_operation() @@ -206,7 +221,19 @@ class ConfigurationPhaseMixin(InstallModlistShortcutDialogMixin): def on_configuration_error(self, error_message): """Handle configuration error on main thread""" self._safe_append_text(f"Configuration failed with error: {error_message}") - MessageService.show_error(self, configuration_failed(str(error_message))) + MessageService.show_error( + self, + configuration_failed( + str(error_message), + context={ + "operation": "install_modlist", + "step": "post_install_configuration", + "modlist_name": self.modlist_name_edit.text().strip(), + "install_dir": self.install_dir_edit.text().strip(), + "workflow_type": "update" if getattr(self, "_is_update_install", False) else "install", + }, + ), + ) # Re-enable all controls on error self._enable_controls_after_operation() diff --git a/jackify/frontends/gui/screens/install_modlist_console.py b/jackify/frontends/gui/screens/install_modlist_console.py index e5e108f..82303dd 100644 --- a/jackify/frontends/gui/screens/install_modlist_console.py +++ b/jackify/frontends/gui/screens/install_modlist_console.py @@ -155,49 +155,6 @@ class ConsoleOutputMixin: self._write_to_log_file(message) return - # CRITICAL: Detect token/auth errors and ALWAYS show them (even when not in debug mode) - token_error_keywords = [ - 'token has expired', - 'token expired', - 'oauth token', - 'authentication failed', - 'unauthorized', - '401', - '403', - 'refresh token', - 'authorization failed', - 'nexus.*premium.*required', - 'premium.*required', - ] - - is_token_error = any(keyword in msg_lower for keyword in token_error_keywords) - if is_token_error: - if not self._token_error_notified: - self._token_error_notified = True - MessageService.critical( - self, - "Authentication Error", - ( - "Nexus Mods authentication has failed. This may be due to:\n\n" - "• OAuth token expired and refresh failed\n" - "• Nexus Premium required for this modlist\n" - "• Network connectivity issues\n\n" - "Please check the console output (Show Details) for more information.\n" - "You may need to re-authorize in Settings." - ), - safety_level="high" - ) - # Also show in console - guidance = ( - "\n[Jackify] CRITICAL: Authentication/Token Error Detected!\n" - "[Jackify] This may cause downloads to stop. Check the error message above.\n" - "[Jackify] If OAuth token expired, go to Settings and re-authorize.\n" - ) - self._safe_append_text(guidance) - # Force console to be visible so user can see the error - if not self.show_details_checkbox.isChecked(): - self.show_details_checkbox.setChecked(True) - # Detect known engine bugs and provide helpful guidance if 'destination array was not long enough' in msg_lower or \ ('argumentexception' in msg_lower and 'downloadmachineurl' in msg_lower): diff --git a/jackify/frontends/gui/screens/install_modlist_installer_thread.py b/jackify/frontends/gui/screens/install_modlist_installer_thread.py index 1979d3c..dce4c82 100644 --- a/jackify/frontends/gui/screens/install_modlist_installer_thread.py +++ b/jackify/frontends/gui/screens/install_modlist_installer_thread.py @@ -3,6 +3,7 @@ InstallerThread: QThread subclass for running jackify-engine install. Signals are defined at class level (required for Qt signal/slot). """ +import json import os import re import threading @@ -12,7 +13,8 @@ from PySide6.QtCore import QThread, Signal import logging from jackify.backend.utils.engine_error_parser import parse_engine_error_line, error_from_exit_code -from jackify.shared.errors import JackifyError +from jackify.backend.utils.cc_content_detector import is_cc_content_error, extract_cc_filename +from jackify.shared.errors import JackifyError, cc_content_missing logger = logging.getLogger(__name__) @@ -24,6 +26,12 @@ class InstallerThread(QThread): progress_updated = Signal(object) installation_finished = Signal(bool, str) premium_required_detected = Signal(str) + # Emitted when engine outputs a full batch of manual download items. + # Payload: list of dicts with keys: file_name, nexus_url/download_url/url, + # expected_size, mod_name, mod_id, file_id, index, total, loop_iteration + manual_download_list_received = Signal(list) + manual_download_phase_complete = Signal() + non_premium_detected = Signal() def __init__(self, modlist, install_dir, downloads_dir, api_key, modlist_name, install_mode='online', progress_state_manager=None, auth_service=None, oauth_info=None): @@ -40,16 +48,81 @@ class InstallerThread(QThread): self.auth_service = auth_service self.oauth_info = oauth_info self._premium_signal_sent = False + self._non_premium_info_sent = False self._engine_output_buffer = [] self._buffer_size = 10 self.last_error: Optional[JackifyError] = None self._raw_stderr_lines: list = [] # bounded ring buffer for non-JSON stderr + self._raw_stdout_lines: list = [] # bounded ring buffer for non-JSON stdout + self._pending_manual_downloads: list = [] # accumulates items until list_complete + self._resource_limit_hint: Optional[str] = None + + @staticmethod + def _is_generic_failure_text(message: Optional[str]) -> bool: + text = (message or "").strip().lower() + if not text: + return True + generic_markers = ( + "did not complete successfully", + "unknown failure", + "an install engine error occurred", + "installation failed due to an engine error", + ) + return any(marker in text for marker in generic_markers) def cancel(self): self.cancelled = True if self.process_manager: self.process_manager.cancel() + def send_continue(self): + """Send the continue command to the engine after manual downloads are ready.""" + if self.process_manager: + sent = self.process_manager.write_stdin('{"command":"continue"}') + if sent: + logger.info("[MDL-1014] Manual download continue command accepted by process stdin") + else: + logger.error("[MDL-9010] Failed to send continue command to engine (stdin unavailable or process exited)") + + def _handle_engine_event(self, line: str) -> bool: + """ + Try to parse a stdout line as an engine workflow event. + Returns True if the line was an event (caller should not emit it as output). + """ + stripped = line.strip() + if not stripped.startswith('{'): + return False + try: + obj = json.loads(stripped) + except (json.JSONDecodeError, ValueError): + return False + + event = obj.get('event') + if not event: + return False + + if event == 'manual_download_required': + self._pending_manual_downloads.append(obj) + return True + + if event == 'manual_download_list_complete': + loop_iter = obj.get('loop_iteration', 1) + items = list(self._pending_manual_downloads) + self._pending_manual_downloads.clear() + for item in items: + item['loop_iteration'] = loop_iter + if items: + logger.info(f"[MDL-1000] Engine manual download list complete | loop_iteration={loop_iter} items={len(items)}") + self.manual_download_list_received.emit(items) + return True + + if event == 'manual_download_phase_complete': + logger.info("[MDL-1015] Engine reported manual download phase complete") + self.manual_download_phase_complete.emit() + return True + + return False + def _read_stderr(self): try: for raw in self.process_manager.proc.stderr: @@ -57,16 +130,116 @@ class InstallerThread(QThread): if not line: continue logger.debug(f"Engine stderr: {line}") + self._raw_stderr_lines.append(line) + if len(self._raw_stderr_lines) > 40: + self._raw_stderr_lines.pop(0) error = parse_engine_error_line(line) if error and self.last_error is None: self.last_error = error else: - self._raw_stderr_lines.append(line) - if len(self._raw_stderr_lines) > 20: - self._raw_stderr_lines.pop(0) + if self.last_error is None and is_cc_content_error(line): + self.last_error = cc_content_missing(extract_cc_filename(line) or "") except Exception as e: logger.debug(f"Stderr reader error: {e}") + def _remember_stdout_line(self, line: str) -> None: + """Keep a bounded tail of meaningful stdout lines for failure diagnostics.""" + cleaned = (line or "").strip() + if not cleaned: + return + if cleaned.startswith("{"): + return + if cleaned.startswith("Installing files ") or cleaned.startswith("Extracting files "): + return + self._raw_stdout_lines.append(cleaned) + if len(self._raw_stdout_lines) > 60: + self._raw_stdout_lines.pop(0) + + def _extract_root_cause_line(self) -> Optional[str]: + """Extract the most actionable error line from stderr/stdout tails.""" + combined = list(reversed(self._raw_stderr_lines)) + list(reversed(self._raw_stdout_lines)) + if not combined: + return None + + ignore_fragments = ( + "installation failed", + "install failed", + "exit code", + "building bsa", + "generating debug caches", + ) + priority_fragments = ( + "too many open files", + "file descriptor", + "resource temporarily unavailable", + "cannot increase file descriptor limit", + "permission denied", + "no space left on device", + "traceback", + "fatal", + "exception", + "error", + "failed", + "could not", + "unable to", + ) + + for raw in combined: + lowered = raw.lower() + if any(fragment in lowered for fragment in ignore_fragments): + continue + if any(fragment in lowered for fragment in priority_fragments): + return raw + + for raw in combined: + lowered = raw.lower() + if any(fragment in lowered for fragment in ignore_fragments): + continue + return raw + + return None + + def _build_failure_message(self, returncode: int) -> str: + """Build a user-facing failure message with the best available root cause.""" + root_cause = self._extract_root_cause_line() + if root_cause: + if self._resource_limit_hint and "file descriptor" not in root_cause.lower(): + return f"{root_cause}\n\nPossible contributing issue: {self._resource_limit_hint}" + return root_cause + + recent_lines = [] + for line in list(reversed(self._raw_stderr_lines)) + list(reversed(self._raw_stdout_lines)): + cleaned = (line or "").strip() + if not cleaned: + continue + lowered = cleaned.lower() + if ( + "install failed" in lowered + or "installation failed" in lowered + or "exit code" in lowered + or "building bsa" in lowered + or "generating debug caches" in lowered + ): + continue + if cleaned not in recent_lines: + recent_lines.append(cleaned) + if len(recent_lines) >= 3: + break + + if recent_lines: + recent_block = "\n- ".join(recent_lines) + return ( + "Install engine reported errors.\n\n" + f"Most recent engine output:\n- {recent_block}" + ) + + if self._resource_limit_hint: + return self._resource_limit_hint + + return ( + "Install failed, but the engine did not provide a specific error line." + ) + def run(self): try: from jackify.backend.core.modlist_operations import get_jackify_engine_path @@ -101,8 +274,25 @@ class InstallerThread(QThread): from jackify.backend.services.nexus_oauth_service import NexusOAuthService env_vars['NEXUS_OAUTH_CLIENT_ID'] = NexusOAuthService.CLIENT_ID env = get_clean_subprocess_env(env_vars) + + # Install-time resource preflight: keep this visible in workflow output so + # users/support see hard-limit constraints even without debug logging. + try: + from jackify.backend.services.resource_manager import ResourceManager + resource_manager = ResourceManager() + status = resource_manager.get_limit_status() + if status.get('current_hard', 0) < status.get('target_limit', 0): + self._resource_limit_hint = ( + f"File descriptor hard limit is {status['current_hard']} " + f"(target {status['target_limit']}); this can cause install failures. " + "Increase ulimit and retry." + ) + self.output_received.emit(f"[WARN] {self._resource_limit_hint}\n") + except Exception as e: + logger.debug(f"Resource preflight check failed: {e}") + from jackify.backend.handlers.subprocess_utils import ProcessManager - self.process_manager = ProcessManager(cmd, env=env, text=False, separate_stderr=True) + self.process_manager = ProcessManager(cmd, env=env, text=False, separate_stderr=True, enable_stdin=True) stderr_thread = threading.Thread(target=self._read_stderr, daemon=True) stderr_thread.start() ansi_escape = re.compile(rb'\x1b\[[0-9;?]*[ -/]*[@-~]') @@ -161,6 +351,8 @@ class InstallerThread(QThread): self._engine_output_buffer.append(decoded.strip()) if len(self._engine_output_buffer) > self._buffer_size: self._engine_output_buffer.pop(0) + if self.last_error is None and is_cc_content_error(decoded): + self.last_error = cc_content_missing(extract_cc_filename(decoded) or "") if self.progress_state_manager: updated = self.progress_state_manager.process_line(decoded) if updated: @@ -179,7 +371,7 @@ class InstallerThread(QThread): line = ansi_escape.sub(b'', line) decoded = line.decode('utf-8', errors='replace') from jackify.backend.utils.nexus_premium_detector import is_non_premium_indicator - is_premium_error, matched_pattern = is_non_premium_indicator(decoded) + is_premium_error, matched_pattern = (False, None) if decoded.strip().startswith('{') else is_non_premium_indicator(decoded) if not self._premium_signal_sent and is_premium_error: self._premium_signal_sent = True logger.warning("=" * 80) @@ -213,9 +405,14 @@ class InstallerThread(QThread): logger.warning("If user HAS Premium, this is a FALSE POSITIVE") logger.warning("=" * 80) self.premium_required_detected.emit(decoded.strip() or "Nexus Premium required") + if not self._non_premium_info_sent and 'non-premium' in decoded.lower() and 'routing' in decoded.lower(): + self._non_premium_info_sent = True + self.non_premium_detected.emit() self._engine_output_buffer.append(decoded.strip()) if len(self._engine_output_buffer) > self._buffer_size: self._engine_output_buffer.pop(0) + if self.last_error is None and is_cc_content_error(decoded): + self.last_error = cc_content_missing(extract_cc_filename(decoded) or "") config_handler = ConfigHandler() debug_mode = config_handler.get('debug_mode', False) if self.progress_state_manager: @@ -225,6 +422,10 @@ class InstallerThread(QThread): if progress_state.active_files and debug_mode: logger.debug(f"DEBUG: Parser detected {len(progress_state.active_files)} active files from line: {decoded[:80]}") self.progress_updated.emit(progress_state) + if self._handle_engine_event(decoded): + last_was_blank = False + continue + self._remember_stdout_line(decoded) if '[FILE_PROGRESS]' in decoded: parts = decoded.split('[FILE_PROGRESS]', 1) if parts[0].strip(): @@ -246,6 +447,7 @@ class InstallerThread(QThread): if parts[0].strip(): self.output_received.emit(parts[0].rstrip()) else: + self._remember_stdout_line(decoded) self.output_received.emit(decoded) stderr_thread.join(timeout=5) returncode = self.process_manager.wait() @@ -265,14 +467,18 @@ class InstallerThread(QThread): except Exception as e: logger.debug(f"DEBUG: Error reading remaining output: {e}") if returncode != 0 and not self.cancelled and self.last_error is None: - stderr_detail = "\n".join(self._raw_stderr_lines[-10:]) if self._raw_stderr_lines else "" - detail = f"Exit code {returncode}.\n\nEngine output:\n{stderr_detail}" if stderr_detail else f"Exit code {returncode}." + stderr_tail = self._raw_stderr_lines[-10:] if self._raw_stderr_lines else [] + stdout_tail = self._raw_stdout_lines[-10:] if self._raw_stdout_lines else [] + combined_tail = stderr_tail + stdout_tail + tail_text = "\n".join(combined_tail) + detail = f"Exit code {returncode}.\n\nEngine output:\n{tail_text}" if tail_text else f"Exit code {returncode}." fallback = error_from_exit_code( returncode, detail, context={ "exit_code": returncode, - "stderr_tail_lines": len(self._raw_stderr_lines[-10:]), + "stderr_tail_lines": len(stderr_tail), + "stdout_tail_lines": len(stdout_tail), }, ) if fallback: @@ -283,8 +489,14 @@ class InstallerThread(QThread): elif returncode == 0: self.installation_finished.emit(True, "Installation completed successfully") else: - error_msg = f"Installation failed (exit code {returncode})" - logger.debug(f"DEBUG: Engine exited with code {returncode}") + if self.last_error: + error_msg = self.last_error.message or "" + if self._is_generic_failure_text(error_msg): + error_msg = self._build_failure_message(returncode) + self.last_error.message = error_msg + else: + error_msg = self._build_failure_message(returncode) + logger.error(f"Engine install failed | exit_code={returncode} summary={error_msg}") self.installation_finished.emit(False, error_msg) except Exception as e: self.installation_finished.emit(False, f"Installation error: {str(e)}") diff --git a/jackify/frontends/gui/screens/install_modlist_nexus.py b/jackify/frontends/gui/screens/install_modlist_nexus.py index f8061ce..a7f76f9 100644 --- a/jackify/frontends/gui/screens/install_modlist_nexus.py +++ b/jackify/frontends/gui/screens/install_modlist_nexus.py @@ -1,6 +1,6 @@ """Nexus authentication methods for InstallModlistScreen (Mixin).""" -from PySide6.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton, QMessageBox, QProgressDialog, QApplication -from PySide6.QtCore import Qt, QTimer, QThread, Signal +from PySide6.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton, QMessageBox, QApplication +from PySide6.QtCore import Qt, QThread, Signal from PySide6.QtGui import QDesktopServices, QGuiApplication import logging import webbrowser @@ -47,7 +47,6 @@ class NexusAuthMixin: layout = QVBoxLayout() layout.setSpacing(15) - # Explanation label info_label = QLabel( "Could not open browser automatically.\n\n" "Please copy the URL below and paste it into your browser:" @@ -56,11 +55,10 @@ class NexusAuthMixin: info_label.setStyleSheet("color: #ccc; font-size: 12px;") layout.addWidget(info_label) - # URL input (read-only but selectable) url_input = QLineEdit() url_input.setText(url) url_input.setReadOnly(True) - url_input.selectAll() # Pre-select text for easy copying + url_input.selectAll() url_input.setStyleSheet(""" QLineEdit { background-color: #1a1a1a; @@ -74,11 +72,9 @@ class NexusAuthMixin: """) layout.addWidget(url_input) - # Button row button_layout = QHBoxLayout() button_layout.addStretch() - # Copy button copy_btn = QPushButton("Copy URL") copy_btn.setStyleSheet(""" QPushButton { @@ -101,7 +97,6 @@ class NexusAuthMixin: copy_btn.clicked.connect(copy_to_clipboard) button_layout.addWidget(copy_btn) - # Close button close_btn = QPushButton("Close") close_btn.setStyleSheet(""" QPushButton { @@ -119,7 +114,113 @@ class NexusAuthMixin: button_layout.addWidget(close_btn) layout.addLayout(button_layout) + dialog.setLayout(layout) + dialog.exec() + def _show_oauth_paste_dialog(self): + """Show dialog for pasting jackify:// callback URL as manual fallback.""" + import urllib.parse + from pathlib import Path + + dialog = QDialog(self) + dialog.setWindowTitle("Paste Callback URL") + dialog.setModal(True) + dialog.setMinimumWidth(560) + + layout = QVBoxLayout() + layout.setSpacing(12) + layout.setContentsMargins(20, 20, 20, 20) + + info_label = QLabel( + "If your browser did not complete the flow automatically:\n\n" + "1. Click Continue in your browser if you have not already.\n" + "2. If a URL starting with jackify:// appears in your browser\n" + " address bar, copy it and paste it below." + ) + info_label.setWordWrap(True) + info_label.setStyleSheet("color: #ccc; font-size: 12px;") + layout.addWidget(info_label) + + url_input = QLineEdit() + url_input.setPlaceholderText("jackify://oauth/callback?code=...&state=...") + url_input.setStyleSheet(""" + QLineEdit { + background-color: #1a1a1a; + color: #3fd0ea; + border: 1px solid #444; + border-radius: 4px; + padding: 8px; + font-family: monospace; + font-size: 11px; + } + """) + layout.addWidget(url_input) + + error_label = QLabel("") + error_label.setStyleSheet("color: #f44336; font-size: 11px;") + error_label.setWordWrap(True) + layout.addWidget(error_label) + + btn_layout = QHBoxLayout() + btn_layout.addStretch() + + submit_btn = QPushButton("Submit") + submit_btn.setStyleSheet(""" + QPushButton { + background-color: #3fd0ea; + color: #000; + border: none; + border-radius: 4px; + padding: 8px 20px; + font-weight: bold; + } + QPushButton:hover { + background-color: #5fdfff; + } + """) + + def on_submit(): + url = url_input.text().strip() + if not url.startswith('jackify://oauth/callback'): + error_label.setText("URL must start with jackify://oauth/callback") + return + parsed = urllib.parse.urlparse(url) + params = urllib.parse.parse_qs(parsed.query) + code = params.get('code', [None])[0] + state = params.get('state', [None])[0] + if not code or not state: + error_label.setText("URL is missing required code or state parameter.") + return + callback_file = Path.home() / ".config" / "jackify" / "oauth_callback.tmp" + try: + callback_file.parent.mkdir(parents=True, exist_ok=True) + callback_file.write_text(f"{code}\n{state}") + logger.info("OAuth callback written via manual paste") + dialog.accept() + except Exception as e: + error_label.setText(f"Failed to write callback: {e}") + + submit_btn.clicked.connect(on_submit) + url_input.returnPressed.connect(on_submit) + btn_layout.addWidget(submit_btn) + + cancel_btn = QPushButton("Cancel") + cancel_btn.setStyleSheet(""" + QPushButton { + background-color: #444; + color: #ccc; + border: none; + border-radius: 4px; + padding: 8px 20px; + } + QPushButton:hover { + background-color: #555; + } + """) + cancel_btn.clicked.connect(dialog.reject) + btn_layout.addWidget(cancel_btn) + + layout.addLayout(btn_layout) dialog.setLayout(layout) dialog.exec() @@ -129,13 +230,11 @@ class NexusAuthMixin: authenticated, method, _ = self.auth_service.get_auth_status() if authenticated and method == 'oauth': - # OAuth is active - offer to revoke reply = MessageService.question(self, "Revoke", "Revoke OAuth authorisation?", safety_level="low") if reply == QMessageBox.Yes: self.auth_service.revoke_oauth() self._update_nexus_status() else: - # Not authorised or using API key - offer to authorise with OAuth reply = MessageService.question(self, "Authorise with Nexus", "Your browser will open for Nexus authorisation.\n\n" "Note: Your browser may ask permission to open 'xdg-open'\n" @@ -146,33 +245,82 @@ class NexusAuthMixin: if reply != QMessageBox.Yes: return - # Create progress dialog - progress = QProgressDialog( - "Waiting for authorisation...\n\nPlease check your browser.", - "Cancel", - 0, 0, - self - ) - progress.setWindowTitle("Nexus OAuth") - progress.setWindowModality(Qt.WindowModal) - progress.setMinimumDuration(0) - progress.setMinimumWidth(400) + # Build waiting dialog with paste fallback always accessible + wait_dialog = QDialog(self) + wait_dialog.setWindowTitle("Nexus OAuth") + wait_dialog.setWindowModality(Qt.WindowModal) + wait_dialog.setMinimumWidth(420) + + wait_layout = QVBoxLayout() + wait_layout.setSpacing(12) + wait_layout.setContentsMargins(20, 20, 20, 20) + + wait_label = QLabel( + "Waiting for authorisation...\n\n" + "Please complete authorisation in your browser.\n\n" + "Your browser may ask permission to open Jackify — click Open or Allow." + ) + wait_label.setWordWrap(True) + wait_label.setStyleSheet("color: #ccc; font-size: 12px;") + wait_layout.addWidget(wait_label) + + wait_layout.addStretch() + + btn_layout = QHBoxLayout() + + paste_btn = QPushButton("Paste callback URL") + paste_btn.setToolTip( + "If your browser shows a jackify:// URL after clicking Continue, paste it here." + ) + paste_btn.setStyleSheet(""" + QPushButton { + background-color: #333; + color: #aaa; + border: 1px solid #555; + border-radius: 4px; + padding: 8px 16px; + } + QPushButton:hover { + background-color: #444; + color: #ccc; + } + """) + paste_btn.clicked.connect(self._show_oauth_paste_dialog) + btn_layout.addWidget(paste_btn) + + btn_layout.addStretch() - # Track cancellation oauth_cancelled = [False] - def on_cancel(): + cancel_btn = QPushButton("Cancel") + cancel_btn.setStyleSheet(""" + QPushButton { + background-color: #444; + color: #ccc; + border: none; + border-radius: 4px; + padding: 8px 16px; + } + QPushButton:hover { + background-color: #555; + } + """) + def on_cancel_click(): oauth_cancelled[0] = True + wait_dialog.close() + cancel_btn.clicked.connect(on_cancel_click) + btn_layout.addWidget(cancel_btn) - progress.canceled.connect(on_cancel) - progress.show() + wait_layout.addLayout(btn_layout) + wait_dialog.setLayout(wait_layout) + wait_dialog.show() QApplication.processEvents() # Create OAuth thread to prevent GUI freeze class OAuthThread(QThread): finished_signal = Signal(bool) message_signal = Signal(str) - manual_url_signal = Signal(str) # Signal when browser fails to open + manual_url_signal = Signal(str) def __init__(self, auth_service, parent=None): super().__init__(parent) @@ -180,9 +328,7 @@ class NexusAuthMixin: def run(self): def show_message(msg): - # Check if this is a "browser failed" message with URL if "Could not open browser" in msg and "Please open this URL manually:" in msg: - # Extract URL from message url_start = msg.find("Please open this URL manually:") + len("Please open this URL manually:") url = msg[url_start:].strip() self.manual_url_signal.emit(url) @@ -194,23 +340,20 @@ class NexusAuthMixin: oauth_thread = OAuthThread(self.auth_service, self) - # Connect message signal to update progress dialog def update_progress_message(msg): if not oauth_cancelled[0]: - progress.setLabelText(f"Waiting for authorisation...\n\n{msg}") + wait_label.setText(f"Waiting for authorisation...\n\n{msg}") QApplication.processEvents() - # Connect manual URL signal to show copyable dialog def show_manual_url_dialog(url): if not oauth_cancelled[0]: - progress.hide() # Hide progress dialog temporarily + wait_dialog.hide() self._show_copyable_url_dialog(url) - progress.show() + wait_dialog.show() oauth_thread.message_signal.connect(update_progress_message) oauth_thread.manual_url_signal.connect(show_manual_url_dialog) - # Wait for thread completion oauth_success = [False] def on_oauth_finished(success): oauth_success[0] = success @@ -218,25 +361,21 @@ class NexusAuthMixin: oauth_thread.finished_signal.connect(on_oauth_finished) oauth_thread.start() - # Wait for thread to finish (non-blocking event loop) while oauth_thread.isRunning(): QApplication.processEvents() - oauth_thread.wait(100) # Check every 100ms + oauth_thread.wait(100) if oauth_cancelled[0]: - # User cancelled - thread will still complete but we ignore result oauth_thread.wait(2000) if oauth_thread.isRunning(): oauth_thread.terminate() break - progress.close() + wait_dialog.close() QApplication.processEvents() self._update_nexus_status() self._enable_controls_after_operation() - # Check success first - if OAuth succeeded, ignore cancellation flag - # (progress dialog close can trigger cancel handler even on success) if oauth_success[0]: _, _, username = self.auth_service.get_auth_status() if username: @@ -250,11 +389,10 @@ class NexusAuthMixin: MessageService.warning( self, "Authorisation Failed", - "OAuth authorisation failed.\n\n" - "If your browser showed a blank page (e.g. Firefox on Steam Deck),\n" - "try again and use 'Paste callback URL' to paste the URL from the address bar.\n\n" - "If you see 'redirect URI mismatch', the OAuth redirect URI must be configured by Nexus.\n\n" - "You can configure an API key in Settings as a fallback.", + "OAuth authorisation timed out.\n\n" + "If your browser shows a URL starting with jackify:// after\n" + "clicking Continue, try again and use 'Paste callback URL'\n" + "during the wait to complete authorisation manually.\n\n" + "If the issue persists, an API key can be configured in Settings.", safety_level="medium" ) - diff --git a/jackify/frontends/gui/screens/install_modlist_postinstall.py b/jackify/frontends/gui/screens/install_modlist_postinstall.py index aca9a0f..10dd293 100644 --- a/jackify/frontends/gui/screens/install_modlist_postinstall.py +++ b/jackify/frontends/gui/screens/install_modlist_postinstall.py @@ -229,6 +229,8 @@ class PostInstallFeedbackMixin: prev_step = self._post_install_sequence[self._post_install_current_step - 1] if prev_step['id'] == 'wine_components' and step['id'] != 'wine_components': self._stop_component_install_pulse() + if prev_step['id'] == 'vnv_bsa_decompress' and step['id'] != 'vnv_bsa_decompress': + self._stop_bsa_decompress_pulse() self._post_install_current_step = idx self._post_install_last_label = step['label'] @@ -250,6 +252,9 @@ class PostInstallFeedbackMixin: self._start_component_install_pulse_with_components(comp_list) break + if step['id'] == 'vnv_bsa_decompress': + self._start_bsa_decompress_pulse() + # Keep Activity window in sync with progress banner # If we're already in wine_components step, check for component list updates # Skip _update_post_install_ui() for wine_components - pulser manages Activity window directly @@ -402,6 +407,7 @@ class PostInstallFeedbackMixin: if not self._post_install_active: return self._stop_component_install_pulse() + self._stop_bsa_decompress_pulse() total = max(1, self._post_install_total_steps) final_step = total if success else max(0, self._post_install_current_step) label = "Post-installation complete" if success else "Post-installation stopped" @@ -468,3 +474,18 @@ class PostInstallFeedbackMixin: if hasattr(self, '_component_install_list'): del self._component_install_list + def _start_bsa_decompress_pulse(self): + """Keep the Activity window alive during long BSA decompression runs.""" + self.file_progress_list.update_or_add_item("__vnv_bsa__", "VNV: Decompressing BSA files...", 0.0) + if not getattr(self, '_bsa_decompress_timer', None): + self._bsa_decompress_timer = QTimer(self) + self._bsa_decompress_timer.timeout.connect(self._bsa_decompress_heartbeat) + self._bsa_decompress_timer.start(250) + + def _bsa_decompress_heartbeat(self): + self.file_progress_list.update_or_add_item("__vnv_bsa__", "VNV: Decompressing BSA files...", 0.0) + + def _stop_bsa_decompress_pulse(self): + if hasattr(self, '_bsa_decompress_timer') and self._bsa_decompress_timer: + self._bsa_decompress_timer.stop() + self._bsa_decompress_timer = None diff --git a/jackify/frontends/gui/screens/install_modlist_progress.py b/jackify/frontends/gui/screens/install_modlist_progress.py index 3ec1429..5e055b9 100644 --- a/jackify/frontends/gui/screens/install_modlist_progress.py +++ b/jackify/frontends/gui/screens/install_modlist_progress.py @@ -8,6 +8,7 @@ from jackify.shared.progress_models import InstallationPhase, OperationType, Ins from jackify.backend.utils.nexus_premium_detector import is_non_premium_indicator import time import logging +import os logger = logging.getLogger(__name__) class ProgressHandlersMixin: @@ -53,6 +54,52 @@ class ProgressHandlersMixin: if hasattr(self, 'install_thread') and self.install_thread: self.install_thread.cancel() + def on_non_premium_detected(self): + """Gate the manual-download dialog until non-premium info has been acknowledged.""" + self._non_premium_gate_enabled = True + self._non_premium_info_acknowledged = False + logger.info("[MDL-1002] Non-premium flow detected; info dialog will show when manual downloads arrive") + + def _show_non_premium_info_dialog(self): + """Show the non-premium information dialog. Blocks (nested event loop) until user clicks OK. + + Called from on_manual_download_list_received, so it only appears when files actually + need manual downloading. The engine is paused waiting for a continue signal at that + point, so process_finished will not fire and close the dialog prematurely. + """ + from PySide6.QtCore import Qt + if getattr(self, '_non_premium_info_dlg', None) is not None: + return + if getattr(self, '_non_premium_info_acknowledged', False): + return + + box = QMessageBox(self) + box.setWindowTitle("Non-Premium Account Detected") + box.setIcon(QMessageBox.Information) + box.setWindowModality(Qt.WindowModal) + box.setTextFormat(Qt.RichText) + box.setText( + "Jackify has detected that your Nexus account does not have Premium." + "

" + "The install will proceed in the following stages:" + "

    " + "
  1. Automatically download any mods available from non-Nexus sources
  2. " + "
  3. After you click OK here, open a manual download dialog listing all remaining manual archives
  4. " + "
" + "When your browser opens a Nexus page, click \"Slow Download\"." + " For non-Nexus manual links, follow the site instructions shown in the page.

" + "Watch folder: Jackify watches the folder shown in that dialog for newly downloaded files. " + "Files detected there are validated and moved automatically into your modlist downloads folder — " + "you do not need to move files manually. If your browser saves to a different location, " + "please set the Watch Folder to that directory before starting the download of mod archives." + ) + box.setStandardButtons(QMessageBox.Ok) + self._non_premium_info_dlg = box + box.exec() + self._non_premium_info_dlg = None + self._non_premium_info_acknowledged = True + logger.info("[MDL-1003] Non-premium information dialog acknowledged by user") + def on_progress_updated(self, progress_state): """R&D: Handle structured progress updates from parser""" # Calculate proper overall progress during BSA building @@ -321,11 +368,16 @@ class ProgressHandlersMixin: from jackify.backend.utils.modlist_meta import write_modlist_meta thread = getattr(self, 'install_thread', None) if thread and getattr(thread, 'install_dir', None) and getattr(thread, 'modlist_name', None): + modlist_version = None + if getattr(thread, 'install_mode', 'online') == 'online': + info = getattr(self, 'selected_modlist_info', None) or {} + modlist_version = info.get('version') write_modlist_meta( thread.install_dir, thread.modlist_name, getattr(self, '_current_game_type', None), install_mode=getattr(thread, 'install_mode', 'online'), + modlist_version=modlist_version, ) except Exception as _meta_err: logger.debug(f"Modlist meta write skipped: {_meta_err}") @@ -337,6 +389,19 @@ class ProgressHandlersMixin: else: # Reset to initial state on failure self.progress_indicator.reset() + cancellation_detected = ( + (isinstance(message, str) and "cancelled by user" in message.lower()) + or bool(getattr(self, '_cancellation_requested', False)) + ) + if cancellation_detected: + self._installation_cancelled = True + logger.info("Installation cancelled by user") + if self.show_details_checkbox.isChecked(): + self._safe_append_text("\nInstallation cancelled by user.") + # Use a distinct non-success code and let process_finished route this + # through the cancellation UX path (not failure path). + self.process_finished(130, QProcess.NormalExit) + return if self._premium_failure_active: message = "Installation stopped because Nexus Premium is required for automated downloads." @@ -359,9 +424,38 @@ class ProgressHandlersMixin: self.cancel_btn.setVisible(True) self.cancel_install_btn.setVisible(False) logger.debug("DEBUG: Button states reset in process_finished") + + # Stop manual download manager if it is still running (e.g. install failed mid-phase) + if getattr(self, '_manual_dl_manager', None) is not None: + try: + self._manual_dl_manager.stop() + except Exception: + pass + self._manual_dl_manager = None + if getattr(self, '_manual_dl_dialog', None) is not None: + try: + self._manual_dl_dialog.close() + except Exception: + pass + self._manual_dl_dialog = None + if getattr(self, '_non_premium_info_dlg', None) is not None: + try: + self._non_premium_info_dlg.close() + except Exception: + pass + self._non_premium_info_dlg = None + self._non_premium_gate_enabled = False + self._non_premium_info_acknowledged = False + self._pending_manual_download_events = None if exit_code == 0: + if getattr(self, "_is_update_install", False): + try: + install_dir = os.path.realpath(self.install_dir_edit.text().strip()) + self._record_post_engine_ini_snapshot_and_diff(install_dir) + except Exception as e: + logger.warning("Update mode: failed post-engine MO2 snapshot/diff: %s", e) # Check if this was an unsupported game game_type = getattr(self, '_current_game_type', None) game_name = getattr(self, '_current_game_name', None) @@ -395,9 +489,22 @@ class ProgressHandlersMixin: ) if reply == QMessageBox.Yes: - # --- Create Steam shortcut BEFORE restarting Steam --- - # Proceed directly to automated prefix creation - self.start_automated_prefix_workflow() + if getattr(self, "_is_update_install", False) and getattr(self, "_existing_shortcut_appid", None): + # Update workflow: reuse existing shortcut and skip shortcut creation/restart path. + modlist_name = self.modlist_name_edit.text().strip() + install_dir = os.path.realpath(self.install_dir_edit.text().strip()) + self._safe_append_text( + f"Update mode: reusing existing Steam shortcut AppID {self._existing_shortcut_appid}." + ) + self.continue_configuration_after_automated_prefix( + self._existing_shortcut_appid, + modlist_name, + install_dir, + None, + ) + else: + # New install workflow: create shortcut and run automated prefix flow. + self.start_automated_prefix_workflow() else: # User selected "No" - show completion message and keep GUI open self._safe_append_text("\nModlist installation completed successfully!") @@ -424,6 +531,10 @@ class ProgressHandlersMixin: logger.warning("Install stopped: Nexus Premium required") self._safe_append_text("\nInstall stopped: Nexus Premium required.") self._premium_failure_active = False + elif getattr(self, '_installation_cancelled', False): + MessageService.information(self, "Installation Cancelled", "The installation was cancelled by the user.", safety_level="low") + self._installation_cancelled = False + self._cancellation_requested = False elif hasattr(self, '_cancellation_requested') and self._cancellation_requested: # User explicitly cancelled via cancel button MessageService.information(self, "Installation Cancelled", "The installation was cancelled by the user.", safety_level="low") @@ -434,14 +545,39 @@ class ProgressHandlersMixin: if "cancelled by user" in last_output.lower(): MessageService.information(self, "Installation Cancelled", "The installation was cancelled by the user.", safety_level="low") else: - logger.error(f"Install failed (exit code {exit_code})") engine_error = getattr(self, '_engine_error', None) if engine_error: self._engine_error = None + logger.error( + "Install failed | exit_code=%s error=%s", + exit_code, + engine_error.message, + ) MessageService.show_error(self, engine_error) + self._safe_append_text(f"\nInstall failed: {engine_error.message}") else: - failure_msg = getattr(self, '_failure_message', None) or f"Exit code {exit_code}." + failure_msg = ( + getattr(self, '_failure_message', None) + or "Install failed, but no specific error details were captured from engine output." + ) self._failure_message = None - MessageService.show_error(self, wabbajack_install_failed(failure_msg)) - self._safe_append_text(f"\nInstall failed (exit code {exit_code}).") + logger.error( + "Install failed | exit_code=%s summary=%s", + exit_code, + failure_msg, + ) + MessageService.show_error( + self, + wabbajack_install_failed( + failure_msg, + context={ + "operation": "install_modlist", + "step": "engine_install", + "exit_code": exit_code, + "modlist_name": self.modlist_name_edit.text().strip(), + "install_dir": self.install_dir_edit.text().strip(), + }, + ), + ) + self._safe_append_text(f"\nInstall failed: {failure_msg}") self.console.moveCursor(QTextCursor.End) diff --git a/jackify/frontends/gui/screens/install_modlist_shortcut_dialog.py b/jackify/frontends/gui/screens/install_modlist_shortcut_dialog.py index 3a0890e..b1b3455 100644 --- a/jackify/frontends/gui/screens/install_modlist_shortcut_dialog.py +++ b/jackify/frontends/gui/screens/install_modlist_shortcut_dialog.py @@ -1,111 +1,83 @@ -"""Steam shortcut conflict dialog and retry workflow for InstallModlistScreen (Mixin).""" -from PySide6.QtWidgets import ( - QDialog, - QVBoxLayout, - QLabel, - QLineEdit, - QPushButton, - QHBoxLayout, -) +"""Steam shortcut conflict handling for InstallModlistScreen (Mixin).""" +import os + +from jackify.frontends.gui.dialogs.existing_setup_dialog import prompt_existing_setup_dialog from jackify.frontends.gui.services.message_service import MessageService class InstallModlistShortcutDialogMixin: """Mixin providing shortcut conflict dialog and retry-with-new-name for InstallModlistScreen.""" + def _restore_controls_after_shortcut_dialog_abort(self): + """Return Install Modlist to a usable state when shortcut resolution is aborted.""" + if hasattr(self, "_abort_install_validation"): + try: + self._abort_install_validation() + return + except Exception: + pass + try: + self._enable_controls_after_operation() + except Exception: + pass + try: + self.cancel_btn.setVisible(True) + self.cancel_install_btn.setVisible(False) + except Exception: + pass + def show_shortcut_conflict_dialog(self, conflicts): - """Show dialog to resolve shortcut name conflicts.""" - conflict_names = [c['name'] for c in conflicts] - conflict_info = f"Found existing Steam shortcut: '{conflict_names[0]}'" - + """Show dialog to resolve existing install / shortcut conflicts.""" + existing_name = conflicts[0].get("name") or self.modlist_name_edit.text().strip() modlist_name = self.modlist_name_edit.text().strip() + install_dir = os.path.realpath(self.install_dir_edit.text().strip()) - dialog = QDialog(self) - dialog.setWindowTitle("Steam Shortcut Conflict") - dialog.setModal(True) - dialog.resize(450, 180) + action, new_name = prompt_existing_setup_dialog( + self, + window_title="Existing Modlist Setup Detected", + heading="Modlist Update or New Install", + body=( + "Jackify detected an existing Steam shortcut for this modlist setup.\n\n" + "If you are updating, repairing, or reconfiguring an existing install, choose " + "'Use Existing Setup'. If you want a separate Steam entry, enter a different " + "name and choose 'Create New Shortcut'." + ), + existing_name=existing_name, + requested_name=modlist_name, + install_dir=install_dir, + field_label="New shortcut name", + reuse_label="Use Existing Setup", + new_label="Create New Shortcut", + cancel_label="Cancel", + ) - dialog.setStyleSheet(""" - QDialog { - background-color: #2b2b2b; - color: #ffffff; - } - QLabel { - color: #ffffff; - font-size: 14px; - padding: 10px 0px; - } - QLineEdit { - background-color: #404040; - color: #ffffff; - border: 2px solid #555555; - border-radius: 4px; - padding: 8px; - font-size: 14px; - selection-background-color: #3fd0ea; - } - QLineEdit:focus { - border-color: #3fd0ea; - } - QPushButton { - background-color: #404040; - color: #ffffff; - border: 2px solid #555555; - border-radius: 4px; - padding: 8px 16px; - font-size: 14px; - min-width: 120px; - } - QPushButton:hover { - background-color: #505050; - border-color: #3fd0ea; - } - QPushButton:pressed { - background-color: #303030; - } - """) + if action == "reuse": + existing_appid = conflicts[0].get("appid") + if not existing_appid: + MessageService.warning( + self, + "Existing Setup Not Found", + "Jackify could not determine the Steam AppID for the existing shortcut.", + ) + self._restore_controls_after_shortcut_dialog_abort() + return + self._safe_append_text(f"Reusing existing Steam shortcut '{existing_name}'.") + self.continue_configuration_after_automated_prefix(int(existing_appid), modlist_name, install_dir, None) + return - layout = QVBoxLayout(dialog) - layout.setContentsMargins(20, 20, 20, 20) - layout.setSpacing(15) - - conflict_label = QLabel(f"{conflict_info}\n\nPlease choose a different name for your shortcut:") - layout.addWidget(conflict_label) - - name_input = QLineEdit(modlist_name) - name_input.selectAll() - layout.addWidget(name_input) - - button_layout = QHBoxLayout() - button_layout.setSpacing(10) - - create_button = QPushButton("Create with New Name") - cancel_button = QPushButton("Cancel") - - button_layout.addStretch() - button_layout.addWidget(cancel_button) - button_layout.addWidget(create_button) - layout.addLayout(button_layout) - - def on_create(): - new_name = name_input.text().strip() + if action == "new": if new_name and new_name != modlist_name: - dialog.accept() self.retry_automated_workflow_with_new_name(new_name) - elif new_name == modlist_name: + return + if new_name == modlist_name: MessageService.warning(self, "Same Name", "Please enter a different name to resolve the conflict.") else: MessageService.warning(self, "Invalid Name", "Please enter a valid shortcut name.") + self._restore_controls_after_shortcut_dialog_abort() + return - def on_cancel(): - dialog.reject() - self._safe_append_text("Shortcut creation cancelled by user") - - create_button.clicked.connect(on_create) - cancel_button.clicked.connect(on_cancel) - name_input.returnPressed.connect(on_create) - - dialog.exec() + self._safe_append_text("Shortcut creation cancelled by user") + self._restore_controls_after_shortcut_dialog_abort() def retry_automated_workflow_with_new_name(self, new_name): """Retry the automated workflow with a new shortcut name.""" diff --git a/jackify/frontends/gui/screens/install_modlist_ui_setup.py b/jackify/frontends/gui/screens/install_modlist_ui_setup.py index bfddabc..2c7baba 100644 --- a/jackify/frontends/gui/screens/install_modlist_ui_setup.py +++ b/jackify/frontends/gui/screens/install_modlist_ui_setup.py @@ -69,6 +69,11 @@ class InstallModlistUISetupMixin: self.file_progress_list = FileProgressList() # Shows all active files (scrolls if needed) self._premium_notice_shown = False self._premium_failure_active = False + self._installation_cancelled = False + self._non_premium_gate_enabled = False + self._non_premium_info_acknowledged = False + self._pending_manual_download_events = None + self._non_premium_info_dlg = None self._stalled_download_start_time = None self._stalled_download_notified = False self._stalled_data_snapshot = 0 @@ -509,4 +514,3 @@ class InstallModlistUISetupMixin: # Now collect all actionable controls after UI is fully built self._collect_actionable_controls() - diff --git a/jackify/frontends/gui/screens/install_modlist_vnv.py b/jackify/frontends/gui/screens/install_modlist_vnv.py index 76b98f4..9aaee40 100644 --- a/jackify/frontends/gui/screens/install_modlist_vnv.py +++ b/jackify/frontends/gui/screens/install_modlist_vnv.py @@ -1,9 +1,9 @@ -"""VNV automation methods for InstallModlistScreen (Mixin).""" -from pathlib import Path -from PySide6.QtCore import QTimer +"""VNV automation methods for InstallModlistScreen (Mixin). + +Delegates to VNVAutomationController for the actual workflow. +""" + import logging -import os -from typing import Optional logger = logging.getLogger(__name__) @@ -12,156 +12,28 @@ class VNVAutomationMixin: """Mixin providing VNV automation methods for InstallModlistScreen.""" def _check_and_run_vnv_automation(self, modlist_name: str, install_dir: str) -> bool: - """Check if VNV automation should run and execute if applicable in background thread - - Args: - modlist_name: Name of the installed modlist - install_dir: Installation directory path + """Check if VNV automation should run and start it if applicable. Returns: True if VNV automation is starting (success dialog should be deferred) False if no VNV automation needed (show success dialog immediately) """ - try: - from jackify.backend.services.vnv_integration_helper import should_offer_vnv_automation - from jackify.backend.handlers.path_handler import PathHandler - from jackify.backend.services.vnv_post_install_service import VNVPostInstallService - from jackify.backend.services.automated_prefix_service import AutomatedPrefixService + from ..services.vnv_automation_controller import VNVAutomationController - # Get paths first (needed for VNV detection) - install_path = Path(install_dir) - - # Quick check before importing more (pass install location for ModOrganizer.ini check) - if not should_offer_vnv_automation(modlist_name, install_path): - return False - - game_paths = PathHandler().find_vanilla_game_paths() - game_root = game_paths.get('Fallout New Vegas') - - if not game_root: - logger.debug("DEBUG: VNV automation skipped - FNV game root not found") - return False - - # Initialize service to check completion status - vnv_service = VNVPostInstallService( - modlist_install_location=install_path, - game_root=game_root, - ttw_installer_path=AutomatedPrefixService.get_ttw_installer_path() - ) - - # Check what's already done - completed = vnv_service.check_already_completed() - # Only skip if ALL three steps are completed - if completed['root_mods'] and completed['4gb_patch'] and completed['bsa_decompressed']: - logger.info("VNV automation steps already completed") - return False - - # Get automation description for confirmation - description = vnv_service.get_automation_description() - - # Show confirmation dialog ON MAIN THREAD (not in worker thread!) - from ..services.message_service import MessageService - from PySide6.QtWidgets import QMessageBox - reply = MessageService.question( - self, - "VNV Post-Install Automation", - description, - critical=False, - safety_level="medium" - ) - - if reply != QMessageBox.Yes: - logger.info("User declined VNV automation") - return False - - # Enable post-install progress tracking for VNV automation - self._begin_post_install_feedback() - - # User confirmed - start automation in background thread - # Note: manual_file_callback is not passed because Qt GUI operations - # cannot be called from a background thread. If downloads fail, - # the service will return instructions for manual download instead. - self._run_vnv_automation_threaded( - modlist_name, - install_path, - game_root - ) - - return True # VNV automation is running, defer success dialog - - except Exception as e: - logger.debug(f"ERROR: Failed to start VNV automation: {e}") - import traceback - logger.debug(f"Traceback: {traceback.format_exc()}") - return False # Error - show success dialog anyway - - def _run_vnv_automation_threaded(self, modlist_name, install_path, game_root): - """Run VNV automation in a background thread with progress updates - - Note: User confirmation should already be obtained before calling this method. - Manual file selection is not supported from background threads - if downloads - fail, the service will return instructions for manual download. - """ - from PySide6.QtCore import QThread, Signal - from jackify.backend.services.vnv_integration_helper import run_vnv_automation_if_applicable - from jackify.backend.services.automated_prefix_service import AutomatedPrefixService - - class VNVAutomationWorker(QThread): - progress_update = Signal(str) - completed = Signal(bool, str) # (success, error_message) - - def __init__(self, modlist_name, install_path, game_root, ttw_installer_path): - super().__init__() - self.modlist_name = modlist_name - self.install_path = install_path - self.game_root = game_root - self.ttw_installer_path = ttw_installer_path - - def run(self): - try: - # User already confirmed, pass lambda that always returns True - # manual_file_callback is None - downloads that fail will return - # instructions for manual download instead of showing Qt dialogs - automation_ran, error = run_vnv_automation_if_applicable( - modlist_name=self.modlist_name, - modlist_install_location=self.install_path, - game_root=self.game_root, - ttw_installer_path=self.ttw_installer_path, - progress_callback=self.progress_update.emit, - manual_file_callback=None, - confirmation_callback=lambda desc: True # Already confirmed on main thread - ) - self.completed.emit(error is None, error or "") - except Exception as e: - import traceback - self.completed.emit(False, f"Exception: {str(e)}\n{traceback.format_exc()}") - - # Create and start worker - self.vnv_worker = VNVAutomationWorker( - modlist_name, - install_path, - game_root, - AutomatedPrefixService.get_ttw_installer_path() + self._vnv_controller = VNVAutomationController() + return self._vnv_controller.attempt( + parent=self, + modlist_name=modlist_name, + install_dir=install_dir, + on_progress=self._safe_append_text, + on_complete=self._on_vnv_complete, + begin_feedback=self._begin_post_install_feedback, + handle_feedback=self._handle_post_install_progress, ) - # Connect signals - self.vnv_worker.progress_update.connect(self._on_vnv_progress) - self.vnv_worker.completed.connect(self._on_vnv_complete) - self.vnv_worker.finished.connect(self.vnv_worker.deleteLater) - - # Start worker - self.vnv_worker.start() - - def _on_vnv_progress(self, message: str): - """Handle VNV automation progress updates""" - self._safe_append_text(message) - # Also update progress indicator, Activity window, and Details window - self._handle_post_install_progress(message) - def _on_vnv_complete(self, success: bool, error: str): - """Handle VNV automation completion and show deferred success dialog""" - # End post-install feedback now that VNV automation is complete - self._end_post_install_feedback(True) + """Handle VNV automation completion and show deferred success dialog.""" + self._end_post_install_feedback(not bool(error)) if not success and error: from ..services.message_service import MessageService @@ -175,32 +47,26 @@ class VNVAutomationMixin: elif success: self._safe_append_text("VNV post-install automation completed successfully") - # Show the deferred success dialog now that VNV automation is complete if hasattr(self, '_pending_success_dialog_params'): params = self._pending_success_dialog_params - del self._pending_success_dialog_params # Clean up + del self._pending_success_dialog_params - # Clear Activity window before showing success dialog self.file_progress_list.clear() - # Show success dialog from ..dialogs import SuccessDialog success_dialog = SuccessDialog( modlist_name=params['modlist_name'], workflow_type="install", time_taken=params['time_taken'], game_name=params['game_name'], - parent=self + parent=self, ) success_dialog.show() - # Show ENB Proton dialog if ENB was detected if params.get('enb_detected'): try: from ..dialogs.enb_proton_dialog import ENBProtonDialog enb_dialog = ENBProtonDialog(modlist_name=params['modlist_name'], parent=self) - enb_dialog.exec() # Modal dialog - blocks until user clicks OK + enb_dialog.exec() except Exception as e: - # Non-blocking: if dialog fails, just log and continue - logger.warning(f"Failed to show ENB dialog: {e}") - + logger.warning("Failed to show ENB dialog: %s", e) diff --git a/jackify/frontends/gui/screens/install_modlist_workflow.py b/jackify/frontends/gui/screens/install_modlist_workflow.py index 74e80a5..7f540f3 100644 --- a/jackify/frontends/gui/screens/install_modlist_workflow.py +++ b/jackify/frontends/gui/screens/install_modlist_workflow.py @@ -1,359 +1,367 @@ """Installation workflow methods for InstallModlistScreen (Mixin).""" -from pathlib import Path -from PySide6.QtCore import QTimer from PySide6.QtWidgets import QMessageBox import logging import os -import re +import shutil import time -from .install_modlist_installer_thread import InstallerThread +from jackify.frontends.gui.dialogs.existing_setup_dialog import prompt_existing_setup_dialog from .install_modlist_output_mixin import InstallModlistOutputMixin -from jackify.backend.services.steam_restart_service import ensure_flatpak_steam_filesystem_access -from jackify.shared.errors import install_dir_create_failed +from .install_modlist_workflow_execution import InstallWorkflowExecutionMixin logger = logging.getLogger(__name__) -class InstallWorkflowMixin(InstallModlistOutputMixin): +class InstallWorkflowMixin(InstallWorkflowExecutionMixin, InstallModlistOutputMixin): """Mixin providing installation workflow methods for InstallModlistScreen.""" - def validate_and_start_install(self): - import time - self._install_workflow_start_time = time.time() - logger.debug('DEBUG: validate_and_start_install called') + @staticmethod + def _normalize_version_token(value: str | None) -> str | None: + """Return a normalized version token for lightweight equality checks.""" + if value is None: + return None + token = str(value).strip() + if not token: + return None + token = token.lstrip("vV") + return token.lower() - # Immediately show "Initialising" status to provide feedback - self.progress_indicator.set_status("Initialising...", 0) - from PySide6.QtWidgets import QApplication - QApplication.processEvents() # Force UI update + @staticmethod + def _normalize_modlist_name(value: str | None) -> str: + return " ".join((value or "").strip().lower().split()) - # Reload config to pick up any settings changes made in Settings dialog - self.config_handler.reload_config() + def _get_requested_modlist_version(self, install_mode: str) -> str | None: + """Return selected modlist version from gallery metadata when available.""" + if install_mode != "online": + return None + info = getattr(self, "selected_modlist_info", None) or {} + return self._normalize_version_token(info.get("version")) - # Check protontricks before proceeding - if not self._check_protontricks(): - self.progress_indicator.reset() + def _evaluate_update_candidate( + self, + modlist_name: str, + install_dir: str, + install_mode: str, + existing_appid: str | None, + ) -> tuple[bool, dict]: + """ + Decide whether update-mode prompt should be shown. + + Policy: + - Require existing shortcut AppID and jackify_meta.json. + - Require modlist identity match (requested name == installed meta name). + - Version relation is informational: + - `different` when both requested/installed versions are available and differ. + - `same` when both are available and equal. + - `unknown` when either side is missing. + """ + from jackify.backend.utils.modlist_meta import read_modlist_meta + + result = { + "eligible": False, + "reason": "unknown", + "requested_version": None, + "installed_version": None, + "version_relation": "unknown", + "installed_name": None, + } + if not existing_appid: + result["reason"] = "missing_shortcut_appid" + return False, result + + meta = read_modlist_meta(install_dir) + if not meta: + result["reason"] = "missing_meta" + return False, result + + installed_name = (meta.get("modlist_name") or "").strip() + result["installed_name"] = installed_name + if self._normalize_modlist_name(installed_name) != self._normalize_modlist_name(modlist_name): + result["reason"] = "modlist_name_mismatch" + return False, result + + requested_version = self._get_requested_modlist_version(install_mode) + installed_version = self._normalize_version_token(meta.get("modlist_version")) + result["requested_version"] = requested_version + result["installed_version"] = installed_version + if requested_version and installed_version: + result["version_relation"] = "same" if requested_version == installed_version else "different" + + result["eligible"] = True + result["reason"] = "eligible" + return True, result + + def _resolve_modorganizer_ini_path(self, install_dir: str) -> str | None: + """Return ModOrganizer.ini path for standard/special layouts.""" + candidates = [ + os.path.join(install_dir, "ModOrganizer.ini"), + os.path.join(install_dir, "files", "ModOrganizer.ini"), + ] + for candidate in candidates: + if os.path.isfile(candidate): + return candidate + return None + + def _capture_mo2_path_state(self, ini_path: str) -> dict[str, str]: + """Capture path-critical keys from ModOrganizer.ini for update comparison.""" + state: dict[str, str] = {} + section = "root" + try: + with open(ini_path, "r", encoding="utf-8", errors="ignore") as f: + for raw_line in f: + line = raw_line.strip() + if not line or line.startswith(("#", ";")): + continue + if line.startswith("[") and line.endswith("]"): + section = line[1:-1].strip() or "root" + continue + if "=" not in line: + continue + key, value = line.split("=", 1) + key = key.strip() + value = value.strip() + key_lower = key.lower() + if ( + key_lower in {"gamepath", "download_directory"} + or key_lower.startswith("binary") + or key_lower.startswith("workingdirectory") + ): + state[f"{section}.{key}"] = value + except Exception as e: + logger.warning("Failed to capture MO2 path state from %s: %s", ini_path, e) + return state + + def _create_update_ini_backup(self, ini_path: str, label: str) -> str | None: + """Create timestamped backup of ModOrganizer.ini for update traceability.""" + try: + timestamp = time.strftime("%Y%m%d_%H%M%S") + backup_path = f"{ini_path}.{label}_{timestamp}.bak" + shutil.copy2(ini_path, backup_path) + return backup_path + except Exception as e: + logger.warning("Failed to create %s backup for %s: %s", label, ini_path, e) + return None + + def _record_pre_update_ini_snapshot(self, install_dir: str) -> None: + """Capture pre-engine MO2 ini snapshot/backup for update-mode comparison.""" + ini_path = self._resolve_modorganizer_ini_path(install_dir) + if not ini_path: + self._update_pre_engine_ini_path = None + self._update_pre_engine_ini_state = {} + logger.warning("Update mode: ModOrganizer.ini not found before engine phase") return - # Disable all controls during installation (except Cancel) - self._disable_controls_during_operation() - - try: - tab_index = self.source_tabs.currentIndex() - install_mode = 'online' - if tab_index == 1: # .wabbajack File tab - modlist = self.file_edit.text().strip() - if not modlist or not os.path.isfile(modlist) or not modlist.endswith('.wabbajack'): - self._abort_with_message( - "warning", - "Invalid Modlist", - "Please select a valid .wabbajack file." - ) - return - install_mode = 'file' - else: - # For online modlists, ALWAYS use machine_url from selected_modlist_info - # Button text is now the display name (title), NOT the machine URL - if not hasattr(self, 'selected_modlist_info') or not self.selected_modlist_info: - self._abort_with_message( - "warning", - "Invalid Modlist", - "Modlist information is missing. Please select the modlist again from the gallery." - ) - return - - machine_url = self.selected_modlist_info.get('machine_url') - if not machine_url: - self._abort_with_message( - "warning", - "Invalid Modlist", - "Modlist information is incomplete. Please select the modlist again from the gallery." - ) - return - - # CRITICAL: Use machine_url, NOT button text - modlist = machine_url - install_dir = self.install_dir_edit.text().strip() - downloads_dir = self.downloads_dir_edit.text().strip() - - # Get authentication token (OAuth or API key) with automatic refresh - api_key, oauth_info = self.auth_service.get_auth_for_engine() - if not api_key: - self._abort_with_message( - "warning", - "Authorisation Required", - "Please authorise with Nexus Mods before installing modlists.\n\n" - "Click the 'Authorise' button above to log in with OAuth,\n" - "or configure an API key in Settings.", - safety_level="medium" - ) - return - - # Log authentication status at install start (Issue #111 diagnostics) - auth_method = self.auth_service.get_auth_method() - logger.info("=" * 60) - logger.info("Authentication Status at Install Start") - logger.info(f"Method: {auth_method or 'UNKNOWN'}") - logger.info(f"Token length: {len(api_key)} chars") - if len(api_key) >= 8: - logger.info(f"Token (partial): {api_key[:4]}...{api_key[-4:]}") - - if auth_method == 'oauth': - token_handler = self.auth_service.token_handler - token_info = token_handler.get_token_info() - if 'expires_in_minutes' in token_info: - logger.info(f"OAuth expires in: {token_info['expires_in_minutes']:.1f} minutes") - if token_info.get('refresh_token_likely_expired'): - logger.warning(f"OAuth refresh token age: {token_info['refresh_token_age_days']:.1f} days (may need re-auth)") - logger.info("=" * 60) - - modlist_name = self.modlist_name_edit.text().strip() - missing_fields = [] - if not modlist_name: - missing_fields.append("Modlist Name") - if not install_dir: - missing_fields.append("Install Directory") - if not downloads_dir: - missing_fields.append("Downloads Directory") - if missing_fields: - self._abort_with_message( - "warning", - "Missing Required Fields", - "Please fill in all required fields before starting the install:\n- " + "\n- ".join(missing_fields) - ) - return - from jackify.backend.handlers.validation_handler import ValidationHandler - validation_handler = ValidationHandler() - is_safe, reason = validation_handler.is_safe_install_directory(Path(install_dir)) - if not is_safe: - from jackify.frontends.gui.dialogs.warning_dialog import WarningDialog - dlg = WarningDialog(reason, parent=self) - result = dlg.exec() - if not result or not dlg.confirmed: - self._abort_install_validation() - return - if not os.path.isdir(install_dir): - from ..services.message_service import MessageService - create = MessageService.question(self, "Create Directory?", - f"The install directory does not exist:\n{install_dir}\n\nWould you like to create it?", - critical=False # Non-critical, won't steal focus - ) - if create == QMessageBox.Yes: - try: - os.makedirs(install_dir, exist_ok=True) - except Exception as e: - MessageService.show_error(self, install_dir_create_failed(install_dir, str(e))) - self._abort_install_validation() - return - else: - self._abort_install_validation() - return - if not os.path.isdir(downloads_dir): - from ..services.message_service import MessageService - create = MessageService.question(self, "Create Directory?", - f"The downloads directory does not exist:\n{downloads_dir}\n\nWould you like to create it?", - critical=False # Non-critical, won't steal focus - ) - if create == QMessageBox.Yes: - try: - os.makedirs(downloads_dir, exist_ok=True) - except Exception as e: - MessageService.show_error(self, install_dir_create_failed(downloads_dir, str(e))) - self._abort_install_validation() - return - else: - self._abort_install_validation() - return - - # Handle resolution saving - resolution = self.resolution_combo.currentText() - if resolution and resolution != "Leave unchanged": - success = self.resolution_service.save_resolution(resolution) - if success: - logger.debug(f"DEBUG: Resolution saved successfully: {resolution}") - else: - logger.debug("DEBUG: Failed to save resolution") - else: - # Clear saved resolution if "Leave unchanged" is selected - if self.resolution_service.has_saved_resolution(): - self.resolution_service.clear_saved_resolution() - logger.debug("DEBUG: Saved resolution cleared") - - ensure_flatpak_steam_filesystem_access(Path(install_dir)) - - # Handle parent directory saving - self._save_parent_directories(install_dir, downloads_dir) - - # Detect game type and check support - game_type = None - game_name = None - - if install_mode == 'file': - # Parse .wabbajack file to get game type - wabbajack_path = Path(modlist) - result = self.wabbajack_parser.parse_wabbajack_game_type(wabbajack_path) - if result: - if isinstance(result, tuple): - game_type, raw_game_type = result - # Get display name for the game - display_names = { - 'skyrim': 'Skyrim', - 'fallout4': 'Fallout 4', - 'falloutnv': 'Fallout New Vegas', - 'oblivion': 'Oblivion', - 'starfield': 'Starfield', - 'oblivion_remastered': 'Oblivion Remastered', - 'enderal': 'Enderal' - } - if game_type == 'unknown' and raw_game_type: - game_name = raw_game_type - else: - game_name = display_names.get(game_type, game_type) - else: - game_type = result - display_names = { - 'skyrim': 'Skyrim', - 'fallout4': 'Fallout 4', - 'falloutnv': 'Fallout New Vegas', - 'oblivion': 'Oblivion', - 'starfield': 'Starfield', - 'oblivion_remastered': 'Oblivion Remastered', - 'enderal': 'Enderal' - } - game_name = display_names.get(game_type, game_type) - else: - # For online modlists, try to get game type from selected modlist - if hasattr(self, 'selected_modlist_info') and self.selected_modlist_info: - game_name = self.selected_modlist_info.get('game', '') - logger.debug(f"DEBUG: Detected game_name from selected_modlist_info: '{game_name}'") - - # Map game name to game type - game_mapping = { - 'skyrim special edition': 'skyrim', - 'skyrim': 'skyrim', - 'fallout 4': 'fallout4', - 'fallout new vegas': 'falloutnv', - 'oblivion': 'oblivion', - 'starfield': 'starfield', - 'oblivion_remastered': 'oblivion_remastered', - 'enderal': 'enderal', - 'enderal special edition': 'enderal' - } - game_type = game_mapping.get(game_name.lower()) - logger.debug(f"DEBUG: Mapped game_name '{game_name}' to game_type: '{game_type}'") - if not game_type: - game_type = 'unknown' - logger.debug(f"DEBUG: Game type not found in mapping, setting to 'unknown'") - else: - logger.debug(f"DEBUG: No selected_modlist_info found") - game_type = 'unknown' - - # Store game type and name for later use - self._current_game_type = game_type - self._current_game_name = game_name - - # Check if game is supported - logger.debug(f"DEBUG: Checking if game_type '{game_type}' is supported") - logger.debug(f"DEBUG: game_type='{game_type}', game_name='{game_name}'") - is_supported = self.wabbajack_parser.is_supported_game(game_type) if game_type else False - logger.debug(f"DEBUG: is_supported_game('{game_type}') returned: {is_supported}") - - if game_type and not is_supported: - logger.debug(f"DEBUG: Game '{game_type}' is not supported, showing dialog") - # Show unsupported game dialog - from ..widgets.unsupported_game_dialog import UnsupportedGameDialog - dialog = UnsupportedGameDialog(self, game_name) - if not dialog.show_dialog(self, game_name): - self._abort_install_validation() - return - - self.console.clear() - self.process_monitor.clear() - - # R&D: Reset progress indicator for new installation - self.progress_indicator.reset() - self.progress_state_manager.reset() - self.file_progress_list.clear() - self.file_progress_list.start_cpu_tracking() # Start tracking CPU during installation - self._premium_notice_shown = False - self._stalled_download_start_time = None - self._stalled_download_notified = False - self._stalled_data_snapshot = 0 - self._token_error_notified = False # Reset token error notification - self._premium_failure_active = False - self._post_install_active = False - self._post_install_current_step = 0 - # Activity tab is always visible (tabs handle visibility automatically) - - # Update button states for installation - self.start_btn.setEnabled(False) - self.cancel_btn.setVisible(False) - self.cancel_install_btn.setVisible(True) - - # CRITICAL: Final safety check - ensure online modlists use machine_url - if install_mode == 'online': - if hasattr(self, 'selected_modlist_info') and self.selected_modlist_info: - expected_machine_url = self.selected_modlist_info.get('machine_url') - if expected_machine_url: - modlist = expected_machine_url # Force use machine_url - else: - self._abort_with_message( - "critical", - "Installation Error", - "Cannot determine modlist machine URL. Please select the modlist again." - ) - return - else: - self._abort_with_message( - "critical", - "Installation Error", - "Modlist information is missing. Please select the modlist again from the gallery." - ) - return - - logger.debug(f'DEBUG: Calling run_modlist_installer with modlist={modlist}, install_dir={install_dir}, downloads_dir={downloads_dir}, install_mode={install_mode}') - self.run_modlist_installer(modlist, install_dir, downloads_dir, api_key, install_mode, oauth_info) - except Exception as e: - logger.debug(f"DEBUG: Exception in validate_and_start_install: {e}") - import traceback - logger.debug(f"DEBUG: Traceback: {traceback.format_exc()}") - # Re-enable all controls after exception - self._enable_controls_after_operation() - self.cancel_btn.setVisible(True) - self.cancel_install_btn.setVisible(False) - logger.debug(f"DEBUG: Controls re-enabled in exception handler") - - def run_modlist_installer(self, modlist, install_dir, downloads_dir, api_key, install_mode='online', oauth_info=None): - logger.debug('DEBUG: run_modlist_installer called - USING THREADED BACKEND WRAPPER') - - # Rotate log file at start of each workflow run (keep 5 backups) - from jackify.backend.handlers.logging_handler import LoggingHandler - log_handler = LoggingHandler() - log_handler.rotate_log_file_per_run(Path(self.modlist_log_path), backup_count=5) - - # Clear console for fresh installation output - self.console.clear() - from jackify import __version__ as jackify_version - self._safe_append_text(f"Jackify v{jackify_version}") - self._safe_append_text("Starting modlist installation with custom progress handling...") - - # Update UI state for installation - self.start_btn.setEnabled(False) - self.cancel_btn.setVisible(False) - self.cancel_install_btn.setVisible(True) - - self.install_thread = InstallerThread( - modlist, install_dir, downloads_dir, api_key, self.modlist_name_edit.text().strip(), install_mode, - progress_state_manager=self.progress_state_manager, # R&D: Pass progress state manager - auth_service=self.auth_service, # Fix Issue #127: Pass auth_service for Premium detection diagnostics - oauth_info=oauth_info # Pass OAuth state for auto-refresh + self._update_pre_engine_ini_path = ini_path + self._update_pre_engine_ini_state = self._capture_mo2_path_state(ini_path) + self._update_pre_engine_ini_backup = self._create_update_ini_backup(ini_path, "pre_update") + logger.info( + "Update mode: captured pre-engine MO2 state | ini=%s backup=%s keys=%d", + ini_path, + self._update_pre_engine_ini_backup, + len(self._update_pre_engine_ini_state), ) - self.install_thread.output_received.connect(self.on_installation_output) - self.install_thread.progress_received.connect(self.on_installation_progress) - self.install_thread.progress_updated.connect(self.on_progress_updated) # R&D: Connect progress update - self.install_thread.installation_finished.connect(self.on_installation_finished) - self.install_thread.premium_required_detected.connect(self.on_premium_required_detected) - # R&D: Pass progress state manager to thread - self.install_thread.progress_state_manager = self.progress_state_manager - self.install_thread.start() + + def _record_post_engine_ini_snapshot_and_diff(self, install_dir: str) -> None: + """Capture post-engine MO2 snapshot and log path-key drift vs pre-engine state.""" + ini_path = self._resolve_modorganizer_ini_path(install_dir) + if not ini_path: + logger.warning("Update mode: ModOrganizer.ini not found after engine phase") + return + + post_state = self._capture_mo2_path_state(ini_path) + post_backup = self._create_update_ini_backup(ini_path, "post_engine") + pre_state = getattr(self, "_update_pre_engine_ini_state", {}) or {} + + changed: list[str] = [] + for key in sorted(set(pre_state) | set(post_state)): + before = pre_state.get(key) + after = post_state.get(key) + if before != after: + changed.append(f"{key}: '{before}' -> '{after}'") + + self._update_ini_path_drift_detected = bool(changed) + self._update_post_engine_ini_state = post_state + self._update_post_engine_ini_path = ini_path + logger.info( + "Update mode: captured post-engine MO2 state | ini=%s backup=%s keys=%d changed=%d", + ini_path, + post_backup, + len(post_state), + len(changed), + ) + if changed: + logger.warning("Update mode: MO2 path-key changes detected after engine phase") + for change in changed: + logger.warning("Update mode INI diff | %s", change) + else: + logger.info("Update mode: no path-key changes detected in ModOrganizer.ini after engine phase") + + def _verify_update_ini_after_configuration(self, install_dir: str) -> None: + """Log-only verification of path-critical ModOrganizer.ini keys after update configuration.""" + summary = self._evaluate_update_ini_verification(install_dir) + if not summary.get("ini_found"): + logger.warning("Update mode verify: ModOrganizer.ini not found after configuration") + return + + logger.info( + "Update mode verify: MO2 ini post-config summary | ini=%s critical_keys=%d empty_critical=%d changed_vs_post_engine=%d changed_vs_pre_engine=%d", + summary["ini_path"], + summary["critical_key_count"], + summary["empty_critical_count"], + summary["changed_vs_post_engine_count"], + summary["changed_vs_pre_engine_count"], + ) + if summary["empty_critical_keys"]: + logger.warning("Update mode verify: empty critical MO2 keys detected") + for key in summary["empty_critical_keys"]: + logger.warning("Update mode verify | empty key: %s", key) + + def _evaluate_update_ini_verification(self, install_dir: str) -> dict: + """ + Evaluate post-config MO2 path-key integrity for update-mode installs. + + Returns a summary dictionary that can be consumed by logging or tests. + """ + ini_path = self._resolve_modorganizer_ini_path(install_dir) + if not ini_path: + return { + "ini_found": False, + "ini_path": None, + "critical_key_count": 0, + "empty_critical_count": 0, + "empty_critical_keys": [], + "changed_vs_post_engine_count": 0, + "changed_vs_pre_engine_count": 0, + "changed_vs_post_engine_keys": [], + "changed_vs_pre_engine_keys": [], + } + + final_state = self._capture_mo2_path_state(ini_path) + pre_state = getattr(self, "_update_pre_engine_ini_state", {}) or {} + post_engine_state = getattr(self, "_update_post_engine_ini_state", {}) or {} + + critical_items = { + k: v + for k, v in final_state.items() + if ( + k.lower().endswith(".gamepath") + or ".binary" in k.lower() + or ".workingdirectory" in k.lower() + or k.lower().endswith(".download_directory") + ) + } + empty_critical = [k for k, v in critical_items.items() if not (v or "").strip()] + + changed_vs_post_engine = [ + k + for k in sorted(set(post_engine_state) | set(final_state)) + if post_engine_state.get(k) != final_state.get(k) + ] + changed_vs_pre_engine = [ + k + for k in sorted(set(pre_state) | set(final_state)) + if pre_state.get(k) != final_state.get(k) + ] + return { + "ini_found": True, + "ini_path": ini_path, + "critical_key_count": len(critical_items), + "empty_critical_count": len(empty_critical), + "empty_critical_keys": empty_critical, + "changed_vs_post_engine_count": len(changed_vs_post_engine), + "changed_vs_pre_engine_count": len(changed_vs_pre_engine), + "changed_vs_post_engine_keys": changed_vs_post_engine, + "changed_vs_pre_engine_keys": changed_vs_pre_engine, + } + + def _find_existing_shortcut_appid(self, modlist_name: str, install_dir: str) -> str | None: + """Return existing Steam shortcut AppID for this install dir/name when present.""" + try: + from jackify.backend.handlers.shortcut_handler import ShortcutHandler + from jackify.backend.services.platform_detection_service import PlatformDetectionService + + platform_service = PlatformDetectionService.get_instance() + shortcut_handler = ShortcutHandler(steamdeck=platform_service.is_steamdeck, verbose=False) + + install_real = os.path.realpath(install_dir) + candidate_exes = [ + os.path.join(install_real, "ModOrganizer.exe"), + os.path.join(install_real, "files", "ModOrganizer.exe"), # Somnium layout + ] + + for exe_path in candidate_exes: + if not os.path.exists(exe_path): + continue + appid = shortcut_handler.get_appid_from_vdf(modlist_name, exe_path) + if appid: + return appid + + # Fallback: match by name + start dir from shortcuts.vdf even if exe moved + for shortcut in shortcut_handler.find_shortcuts_by_exe("ModOrganizer.exe"): + if ( + (shortcut.get("AppName", "").strip() == modlist_name.strip()) + and os.path.realpath(shortcut.get("StartDir", "")) == install_real + ): + raw_appid = shortcut.get("appid") + if raw_appid is not None: + return str(int(raw_appid) & 0xFFFFFFFF) + except Exception as e: + logger.warning("Update detection: failed shortcut lookup: %s", e) + return None + + def _prompt_update_or_new_install( + self, + modlist_name: str, + install_dir: str, + update_meta: dict | None = None, + ) -> str: + """Prompt user when update conditions are met. Returns: 'update'|'new'|'cancel'.""" + version_note = "" + if update_meta: + relation = update_meta.get("version_relation") + req = update_meta.get("requested_version") + inst = update_meta.get("installed_version") + if relation == "different": + version_note = ( + f"\n\nDetected version change: installed v{inst} -> selected v{req}." + ) + elif relation == "same" and inst: + version_note = ( + f"\n\nDetected same version (v{inst}). " + "Use the existing setup if you are repairing or reconfiguring this install." + ) + + body = ( + "Jackify detected an existing modlist installation in the selected directory.\n\n" + "Choose 'Use Existing Setup' to continue with the current install and Steam shortcut. " + "Choose 'Create New Shortcut' only if you want a separate Steam entry with a different name." + f"{version_note}" + ) + + action, new_name = prompt_existing_setup_dialog( + self, + window_title="Existing Modlist Setup Detected", + heading="Use Existing Setup or Create a New Shortcut", + body=body, + existing_name=modlist_name, + requested_name=modlist_name, + install_dir=install_dir, + field_label="New shortcut name", + reuse_label="Use Existing Setup", + new_label="Create New Shortcut", + cancel_label="Cancel", + ) + + if action == "reuse": + return "update" + if action == "new": + if not new_name: + MessageBox = QMessageBox # keep local usage explicit + MessageBox.warning(self, "Invalid Name", "Please enter a valid shortcut name.") + return "cancel" + if new_name == modlist_name: + QMessageBox.warning(self, "Same Name", "Please enter a different name to create a separate shortcut.") + return "cancel" + self.modlist_name_edit.setText(new_name) + return "new" + return "cancel" diff --git a/jackify/frontends/gui/screens/install_modlist_workflow_execution.py b/jackify/frontends/gui/screens/install_modlist_workflow_execution.py new file mode 100644 index 0000000..aab452b --- /dev/null +++ b/jackify/frontends/gui/screens/install_modlist_workflow_execution.py @@ -0,0 +1,516 @@ +"""Execution workflow methods for InstallModlistScreen (Mixin).""" + +from pathlib import Path +from PySide6.QtWidgets import QMessageBox +import logging +import os + +from .install_modlist_installer_thread import InstallerThread +from jackify.backend.services.steam_restart_service import ensure_flatpak_steam_filesystem_access +from jackify.shared.errors import install_dir_create_failed + +logger = logging.getLogger(__name__) + + +class InstallWorkflowExecutionMixin: + """Mixin containing install-run and manual-download dialog execution methods.""" + def validate_and_start_install(self): + import time + self._install_workflow_start_time = time.time() + logger.debug('DEBUG: validate_and_start_install called') + + # Immediately show "Initialising" status to provide feedback + self.progress_indicator.set_status("Initialising...", 0) + from PySide6.QtWidgets import QApplication + QApplication.processEvents() # Force UI update + + # Reload config to pick up any settings changes made in Settings dialog + self.config_handler.reload_config() + + # Check protontricks before proceeding + if not self._check_protontricks(): + self.progress_indicator.reset() + return + + # Disable all controls during installation (except Cancel) + self._disable_controls_during_operation() + + try: + tab_index = self.source_tabs.currentIndex() + install_mode = 'online' + if tab_index == 1: # .wabbajack File tab + modlist = self.file_edit.text().strip() + if not modlist or not os.path.isfile(modlist) or not modlist.endswith('.wabbajack'): + self._abort_with_message( + "warning", + "Invalid Modlist", + "Please select a valid .wabbajack file." + ) + return + install_mode = 'file' + else: + # For online modlists, ALWAYS use machine_url from selected_modlist_info + # Button text is now the display name (title), NOT the machine URL + if not hasattr(self, 'selected_modlist_info') or not self.selected_modlist_info: + self._abort_with_message( + "warning", + "Invalid Modlist", + "Modlist information is missing. Please select the modlist again from the gallery." + ) + return + + machine_url = self.selected_modlist_info.get('machine_url') + if not machine_url: + self._abort_with_message( + "warning", + "Invalid Modlist", + "Modlist information is incomplete. Please select the modlist again from the gallery." + ) + return + + # CRITICAL: Use machine_url, NOT button text + modlist = machine_url + install_dir = self.install_dir_edit.text().strip() + downloads_dir = self.downloads_dir_edit.text().strip() + + # Get authentication token (OAuth or API key) with automatic refresh + api_key, oauth_info = self.auth_service.get_auth_for_engine() + if not api_key: + self._abort_with_message( + "warning", + "Authorisation Required", + "Please authorise with Nexus Mods before installing modlists.\n\n" + "Click the 'Authorise' button above to log in with OAuth,\n" + "or configure an API key in Settings.", + safety_level="medium" + ) + return + + # Log authentication status at install start (Issue #111 diagnostics) + auth_method = self.auth_service.get_auth_method() + logger.info("=" * 60) + logger.info("Authentication Status at Install Start") + logger.info(f"Method: {auth_method or 'UNKNOWN'}") + logger.info(f"Token length: {len(api_key)} chars") + if len(api_key) >= 8: + logger.info(f"Token (partial): {api_key[:4]}...{api_key[-4:]}") + + if auth_method == 'oauth': + token_handler = self.auth_service.token_handler + token_info = token_handler.get_token_info() + if 'expires_in_minutes' in token_info: + logger.info(f"OAuth expires in: {token_info['expires_in_minutes']:.1f} minutes") + if token_info.get('refresh_token_likely_expired'): + logger.warning(f"OAuth refresh token age: {token_info['refresh_token_age_days']:.1f} days (may need re-auth)") + logger.info("=" * 60) + + modlist_name = self.modlist_name_edit.text().strip() + missing_fields = [] + if not modlist_name: + missing_fields.append("Modlist Name") + if not install_dir: + missing_fields.append("Install Directory") + if not downloads_dir: + missing_fields.append("Downloads Directory") + if missing_fields: + self._abort_with_message( + "warning", + "Missing Required Fields", + "Please fill in all required fields before starting the install:\n- " + "\n- ".join(missing_fields) + ) + return + from jackify.backend.handlers.validation_handler import ValidationHandler + validation_handler = ValidationHandler() + is_safe, reason = validation_handler.is_safe_install_directory(Path(install_dir)) + if not is_safe: + from jackify.frontends.gui.dialogs.warning_dialog import WarningDialog + dlg = WarningDialog(reason, parent=self) + result = dlg.exec() + if not result or not dlg.confirmed: + self._abort_install_validation() + return + if not os.path.isdir(install_dir): + from ..services.message_service import MessageService + create = MessageService.question(self, "Create Directory?", + f"The install directory does not exist:\n{install_dir}\n\nWould you like to create it?", + critical=False # Non-critical, won't steal focus + ) + if create == QMessageBox.Yes: + try: + os.makedirs(install_dir, exist_ok=True) + except Exception as e: + MessageService.show_error(self, install_dir_create_failed(install_dir, str(e))) + self._abort_install_validation() + return + else: + self._abort_install_validation() + return + if not os.path.isdir(downloads_dir): + from ..services.message_service import MessageService + create = MessageService.question(self, "Create Directory?", + f"The downloads directory does not exist:\n{downloads_dir}\n\nWould you like to create it?", + critical=False # Non-critical, won't steal focus + ) + if create == QMessageBox.Yes: + try: + os.makedirs(downloads_dir, exist_ok=True) + except Exception as e: + MessageService.show_error(self, install_dir_create_failed(downloads_dir, str(e))) + self._abort_install_validation() + return + else: + self._abort_install_validation() + return + + # Handle resolution saving + resolution = self.resolution_combo.currentText() + if resolution and resolution != "Leave unchanged": + success = self.resolution_service.save_resolution(resolution) + if success: + logger.debug(f"DEBUG: Resolution saved successfully: {resolution}") + else: + logger.debug("DEBUG: Failed to save resolution") + else: + # Clear saved resolution if "Leave unchanged" is selected + if self.resolution_service.has_saved_resolution(): + self.resolution_service.clear_saved_resolution() + logger.debug("DEBUG: Saved resolution cleared") + + ensure_flatpak_steam_filesystem_access(Path(install_dir)) + + # Handle parent directory saving + self._save_parent_directories(install_dir, downloads_dir) + + # Detect game type and check support + game_type = None + game_name = None + + if install_mode == 'file': + # Parse .wabbajack file to get game type + wabbajack_path = Path(modlist) + result = self.wabbajack_parser.parse_wabbajack_game_type(wabbajack_path) + if result: + if isinstance(result, tuple): + game_type, raw_game_type = result + # Get display name for the game + display_names = { + 'skyrim': 'Skyrim', + 'fallout4': 'Fallout 4', + 'falloutnv': 'Fallout New Vegas', + 'oblivion': 'Oblivion', + 'starfield': 'Starfield', + 'oblivion_remastered': 'Oblivion Remastered', + 'enderal': 'Enderal' + } + if game_type == 'unknown' and raw_game_type: + game_name = raw_game_type + else: + game_name = display_names.get(game_type, game_type) + else: + game_type = result + display_names = { + 'skyrim': 'Skyrim', + 'fallout4': 'Fallout 4', + 'falloutnv': 'Fallout New Vegas', + 'oblivion': 'Oblivion', + 'starfield': 'Starfield', + 'oblivion_remastered': 'Oblivion Remastered', + 'enderal': 'Enderal' + } + game_name = display_names.get(game_type, game_type) + else: + # For online modlists, try to get game type from selected modlist + if hasattr(self, 'selected_modlist_info') and self.selected_modlist_info: + game_name = self.selected_modlist_info.get('game', '') + logger.debug(f"DEBUG: Detected game_name from selected_modlist_info: '{game_name}'") + + # Map game name to game type + game_mapping = { + 'skyrim special edition': 'skyrim', + 'skyrim': 'skyrim', + 'fallout 4': 'fallout4', + 'fallout new vegas': 'falloutnv', + 'oblivion': 'oblivion', + 'starfield': 'starfield', + 'oblivion_remastered': 'oblivion_remastered', + 'enderal': 'enderal', + 'enderal special edition': 'enderal' + } + game_type = game_mapping.get(game_name.lower()) + logger.debug(f"DEBUG: Mapped game_name '{game_name}' to game_type: '{game_type}'") + if not game_type: + game_type = 'unknown' + logger.debug(f"DEBUG: Game type not found in mapping, setting to 'unknown'") + else: + logger.debug(f"DEBUG: No selected_modlist_info found") + game_type = 'unknown' + + # Store game type and name for later use + self._current_game_type = game_type + self._current_game_name = game_name + + # Check if game is supported + logger.debug(f"DEBUG: Checking if game_type '{game_type}' is supported") + logger.debug(f"DEBUG: game_type='{game_type}', game_name='{game_name}'") + is_supported = self.wabbajack_parser.is_supported_game(game_type) if game_type else False + logger.debug(f"DEBUG: is_supported_game('{game_type}') returned: {is_supported}") + + if game_type and not is_supported: + logger.debug(f"DEBUG: Game '{game_type}' is not supported, showing dialog") + # Show unsupported game dialog + from ..widgets.unsupported_game_dialog import UnsupportedGameDialog + dialog = UnsupportedGameDialog(self, game_name) + if not dialog.show_dialog(self, game_name): + self._abort_install_validation() + return + + self.console.clear() + self.process_monitor.clear() + + # Collapse Show Details if it was left open by the previous run. + if self.show_details_checkbox.isChecked(): + self.show_details_checkbox.blockSignals(True) + self.show_details_checkbox.setChecked(False) + self.show_details_checkbox.blockSignals(False) + from PySide6.QtCore import Qt as _Qt + self._toggle_console_visibility(_Qt.Unchecked) + + # R&D: Reset progress indicator for new installation + self.progress_indicator.reset() + self.progress_state_manager.reset() + self.file_progress_list.clear() + self.file_progress_list.start_cpu_tracking() # Start tracking CPU during installation + self._is_update_install = False + self._existing_shortcut_appid = None + self._premium_notice_shown = False + self._stalled_download_start_time = None + self._stalled_download_notified = False + self._stalled_data_snapshot = 0 + self._token_error_notified = False # Reset token error notification + self._premium_failure_active = False + self._installation_cancelled = False + self._non_premium_gate_enabled = False + self._non_premium_info_acknowledged = False + self._pending_manual_download_events = None + self._post_install_active = False + self._post_install_current_step = 0 + # Activity tab is always visible (tabs handle visibility automatically) + + # Update button states for installation + self.start_btn.setEnabled(False) + self.cancel_btn.setVisible(False) + self.cancel_install_btn.setVisible(True) + + # Detect update-vs-new workflow before starting engine install. + from jackify.backend.utils.modlist_meta import JACKIFY_META_FILE + install_real = os.path.realpath(install_dir) + meta_exists = (Path(install_real) / JACKIFY_META_FILE).exists() + existing_appid = self._find_existing_shortcut_appid(modlist_name, install_real) + if meta_exists and existing_appid: + eligible, update_meta = self._evaluate_update_candidate( + modlist_name, + install_real, + install_mode, + existing_appid, + ) + if not eligible: + logger.info( + "Update mode not offered | reason=%s requested_name=%s installed_name=%s", + update_meta.get("reason"), + modlist_name, + update_meta.get("installed_name"), + ) + else: + logger.info( + "Update mode candidate | version_relation=%s requested_version=%s installed_version=%s", + update_meta.get("version_relation"), + update_meta.get("requested_version"), + update_meta.get("installed_version"), + ) + decision = self._prompt_update_or_new_install(modlist_name, install_real, update_meta) + if decision == "cancel": + self._abort_install_validation() + return + if decision == "new": + from ..services.message_service import MessageService + + MessageService.warning( + self, + "Shortcut Name Already Exists", + "A Steam shortcut with this name already points to this install directory.\n\n" + "For a new install, choose a different Modlist Name before starting.", + safety_level="medium", + ) + self._abort_install_validation() + return + # update + self._is_update_install = True + self._existing_shortcut_appid = existing_appid + self._safe_append_text( + f"Update mode selected. Reusing existing Steam shortcut AppID {existing_appid}." + ) + self._record_pre_update_ini_snapshot(install_real) + + # CRITICAL: Final safety check - ensure online modlists use machine_url + if install_mode == 'online': + if hasattr(self, 'selected_modlist_info') and self.selected_modlist_info: + expected_machine_url = self.selected_modlist_info.get('machine_url') + if expected_machine_url: + modlist = expected_machine_url # Force use machine_url + else: + self._abort_with_message( + "critical", + "Installation Error", + "Cannot determine modlist machine URL. Please select the modlist again." + ) + return + else: + self._abort_with_message( + "critical", + "Installation Error", + "Modlist information is missing. Please select the modlist again from the gallery." + ) + return + + logger.debug(f'DEBUG: Calling run_modlist_installer with modlist={modlist}, install_dir={install_dir}, downloads_dir={downloads_dir}, install_mode={install_mode}') + self.run_modlist_installer(modlist, install_dir, downloads_dir, api_key, install_mode, oauth_info) + except Exception as e: + logger.debug(f"DEBUG: Exception in validate_and_start_install: {e}") + import traceback + logger.debug(f"DEBUG: Traceback: {traceback.format_exc()}") + # Re-enable all controls after exception + self._enable_controls_after_operation() + self.cancel_btn.setVisible(True) + self.cancel_install_btn.setVisible(False) + logger.debug(f"DEBUG: Controls re-enabled in exception handler") + + def run_modlist_installer(self, modlist, install_dir, downloads_dir, api_key, install_mode='online', oauth_info=None): + logger.debug('DEBUG: run_modlist_installer called - USING THREADED BACKEND WRAPPER') + + # Rotate log file at start of each workflow run (keep 5 backups) + from jackify.backend.handlers.logging_handler import LoggingHandler + log_handler = LoggingHandler() + log_handler.rotate_log_file_per_run(Path(self.modlist_log_path), backup_count=5) + + # Clear console for fresh installation output + self.console.clear() + from jackify import __version__ as jackify_version + self._safe_append_text(f"Jackify v{jackify_version}") + self._safe_append_text("Starting modlist installation with custom progress handling...") + + # Update UI state for installation + self.start_btn.setEnabled(False) + self.cancel_btn.setVisible(False) + self.cancel_install_btn.setVisible(True) + + self._downloads_dir = downloads_dir + self.install_thread = InstallerThread( + modlist, install_dir, downloads_dir, api_key, self.modlist_name_edit.text().strip(), install_mode, + progress_state_manager=self.progress_state_manager, # R&D: Pass progress state manager + auth_service=self.auth_service, # Fix Issue #127: Pass auth_service for Premium detection diagnostics + oauth_info=oauth_info # Pass OAuth state for auto-refresh + ) + self.install_thread.output_received.connect(self.on_installation_output) + self.install_thread.progress_received.connect(self.on_installation_progress) + self.install_thread.progress_updated.connect(self.on_progress_updated) # R&D: Connect progress update + self.install_thread.installation_finished.connect(self.on_installation_finished) + self.install_thread.premium_required_detected.connect(self.on_premium_required_detected) + self.install_thread.non_premium_detected.connect(self.on_non_premium_detected) + self.install_thread.manual_download_list_received.connect(self.on_manual_download_list_received) + # R&D: Pass progress state manager to thread + self.install_thread.progress_state_manager = self.progress_state_manager + self.install_thread.finished.connect(self.install_thread.deleteLater) + self.install_thread.start() + + def on_manual_download_list_received(self, events: list) -> None: + """Show the manual download dialog when the engine emits a batch of missing files.""" + try: + # Show non-premium info dialog synchronously before the file list. + # The engine is paused waiting for a continue signal at this point, + # so process_finished will not fire during exec() and close it prematurely. + if getattr(self, '_non_premium_gate_enabled', False) and not getattr(self, '_non_premium_info_acknowledged', False): + self._show_non_premium_info_dialog() + logger.info(f"[MDL-1005] Showing manual download dialog for batch | items={len(events)}") + self._show_manual_download_dialog(events) + except Exception as exc: + logger.error(f"Manual download dialog setup failed: {exc}", exc_info=True) + self._safe_append_text(f"\n[ERROR] Manual download dialog failed to open: {exc}\n") + + def _flush_pending_manual_download_events(self) -> None: + events = getattr(self, '_pending_manual_download_events', None) + if not events: + return + self._pending_manual_download_events = None + logger.info(f"[MDL-1007] Releasing queued manual download batch after acknowledgement | items={len(events)}") + self._show_manual_download_dialog(events) + + def _show_manual_download_dialog(self, events: list) -> None: + from pathlib import Path as _Path + from jackify.backend.handlers.config_handler import ConfigHandler + from jackify.backend.services.manual_download_manager import ManualDownloadManager + from jackify.frontends.gui.dialogs.manual_download_dialog import ManualDownloadDialog + + cfg_watch = ConfigHandler().get("manual_download_watch_directory", None) + watch_dir = None + if cfg_watch: + cfg_path = _Path(str(cfg_watch)).expanduser() + if cfg_path.is_dir(): + watch_dir = cfg_path + if watch_dir is None: + xdg_dl = Path(os.environ.get('XDG_DOWNLOAD_DIR', '')) if os.environ.get('XDG_DOWNLOAD_DIR') else None + watch_dir = xdg_dl if (xdg_dl and xdg_dl.is_dir()) else _Path.home() / 'Downloads' + dl_dir = _Path(self._downloads_dir) if hasattr(self, '_downloads_dir') else watch_dir + + loop_iteration = events[0].get('loop_iteration', 1) if events else 1 + count = len(events) + raw_limit = ConfigHandler().get('manual_download_concurrent_limit', 2) + try: + concurrent_limit = int(raw_limit) + except (TypeError, ValueError): + concurrent_limit = 2 + concurrent_limit = max(1, min(5, concurrent_limit)) + + self._safe_append_text( + f"\n[Manual Download Required] {count} file(s) need manual download.\n" + f"Opening download dialog — check your taskbar if it does not appear in front.\n" + ) + logger.info( + f"[MDL-1006] Manual download protocol initialized | count={count} " + f"loop_iteration={loop_iteration} watch_dir={watch_dir} downloads_dir={dl_dir}" + ) + + # New install run: start with a fresh manager/dialog to avoid stale statuses from prior runs. + if loop_iteration == 1: + if getattr(self, '_manual_dl_manager', None) is not None: + try: + self._manual_dl_manager.stop() + except Exception: + pass + self._manual_dl_manager = None + if getattr(self, '_manual_dl_dialog', None) is not None: + try: + self._manual_dl_dialog.close() + except Exception: + pass + self._manual_dl_dialog = None + + if not hasattr(self, '_manual_dl_manager') or self._manual_dl_manager is None: + self._manual_dl_manager = ManualDownloadManager( + modlist_download_dir=dl_dir, + watch_directory=watch_dir, + concurrent_limit=concurrent_limit, + on_send_continue=self.install_thread.send_continue, + ) + self._manual_dl_dialog = ManualDownloadDialog( + manager=self._manual_dl_manager, + modlist_name=self.modlist_name_edit.text().strip() if hasattr(self, 'modlist_name_edit') else '', + watch_directory=watch_dir, + concurrent_limit=concurrent_limit, + parent=self, + ) + + self._manual_dl_manager.load_items(events, loop_iteration) + self._manual_dl_dialog.load_items(self._manual_dl_manager.items) + + if not self._manual_dl_dialog.isVisible(): + self._manual_dl_dialog.show() diff --git a/jackify/frontends/gui/screens/install_ttw.py b/jackify/frontends/gui/screens/install_ttw.py index 91590eb..3f69e71 100644 --- a/jackify/frontends/gui/screens/install_ttw.py +++ b/jackify/frontends/gui/screens/install_ttw.py @@ -304,25 +304,28 @@ class InstallTTWScreen(ScreenBackMixin, TTWUISetupMixin, TTWIntegrationMixin, TT """Clean up any running processes when the window closes or is cancelled""" logger.debug("DEBUG: cleanup_processes called - cleaning up InstallationThread and other processes") - # Clean up InstallationThread if running - if hasattr(self, 'install_thread') and self.install_thread.isRunning(): + # install_thread uses cancel() for cooperative shutdown before terminate. + if hasattr(self, 'install_thread') and self.install_thread and self.install_thread.isRunning(): logger.debug("DEBUG: Cancelling running InstallationThread") self.install_thread.cancel() - self.install_thread.wait(3000) # Wait up to 3 seconds + self.install_thread.wait(3000) if self.install_thread.isRunning(): self.install_thread.terminate() - - # Clean up other threads - threads = [ - 'prefix_thread', 'config_thread', 'fetch_thread' - ] - for thread_name in threads: - if hasattr(self, thread_name): - thread = getattr(self, thread_name) - if thread and thread.isRunning(): - logger.debug(f"DEBUG: Terminating {thread_name}") - thread.terminate() - thread.wait(1000) # Wait up to 1 second + self.install_thread.wait(2000) + self.install_thread = None + + from PySide6.QtCore import QThread + for attr_name, value in list(vars(self).items()): + if attr_name == 'install_thread': + continue + try: + if isinstance(value, QThread) and value.isRunning(): + logger.debug(f"DEBUG: Terminating {attr_name}") + value.terminate() + value.wait(2000) + setattr(self, attr_name, None) + except Exception: + pass def cancel_installation(self): """Cancel the currently running installation""" @@ -353,7 +356,7 @@ class InstallTTWScreen(ScreenBackMixin, TTWUISetupMixin, TTWIntegrationMixin, TT """) # Cancel the installation thread if it exists - if hasattr(self, 'install_thread') and self.install_thread.isRunning(): + if hasattr(self, 'install_thread') and self.install_thread and self.install_thread.isRunning(): self.install_thread.cancel() self.install_thread.wait(3000) # Wait up to 3 seconds for graceful shutdown if self.install_thread.isRunning(): @@ -361,7 +364,7 @@ class InstallTTWScreen(ScreenBackMixin, TTWUISetupMixin, TTWIntegrationMixin, TT self.install_thread.wait(1000) # Cancel the automated prefix thread if it exists - if hasattr(self, 'prefix_thread') and self.prefix_thread.isRunning(): + if hasattr(self, 'prefix_thread') and self.prefix_thread and self.prefix_thread.isRunning(): self.prefix_thread.terminate() self.prefix_thread.wait(3000) # Wait up to 3 seconds for graceful shutdown if self.prefix_thread.isRunning(): @@ -369,7 +372,7 @@ class InstallTTWScreen(ScreenBackMixin, TTWUISetupMixin, TTWIntegrationMixin, TT self.prefix_thread.wait(1000) # Cancel the configuration thread if it exists - if hasattr(self, 'config_thread') and self.config_thread.isRunning(): + if hasattr(self, 'config_thread') and self.config_thread and self.config_thread.isRunning(): self.config_thread.terminate() self.config_thread.wait(3000) # Wait up to 3 seconds for graceful shutdown if self.config_thread.isRunning(): diff --git a/jackify/frontends/gui/screens/install_ttw_output.py b/jackify/frontends/gui/screens/install_ttw_output.py index b93bc4d..945504f 100644 --- a/jackify/frontends/gui/screens/install_ttw_output.py +++ b/jackify/frontends/gui/screens/install_ttw_output.py @@ -51,6 +51,9 @@ class TTWOutputMixin: is_file_op = any(ext in lower_cleaned for ext in ['.ogg', '.mp3', '.bsa', '.dds', '.nif', '.kf', '.hkx']) is_noise = cleaned.strip().upper() in ['OK', 'OK.', 'OK!', 'DONE', 'DONE.', 'SUCCESS', 'SUCCESS.'] + if is_error and 'cannot get directory path for location type' in lower_cleaned: + self._ttw_unclean_game_dir_detected = True + should_show = (is_error or is_warning or is_milestone) or (self.show_details_checkbox.isChecked() and not is_file_op and not is_noise) if should_show: diff --git a/jackify/frontends/gui/screens/install_ttw_thread.py b/jackify/frontends/gui/screens/install_ttw_thread.py index b2e7917..f184b63 100644 --- a/jackify/frontends/gui/screens/install_ttw_thread.py +++ b/jackify/frontends/gui/screens/install_ttw_thread.py @@ -143,7 +143,11 @@ class TTWInstallationThread(QThread): elif returncode == 0: self.installation_finished.emit(True, "TTW installation completed successfully!") else: - self.installation_finished.emit(False, f"TTW installation failed with exit code {returncode}") + self.installation_finished.emit( + False, + f"TTW installer exited unexpectedly (code {returncode}). " + "Review the recent console output for the failing step." + ) except Exception as e: import traceback diff --git a/jackify/frontends/gui/screens/install_ttw_workflow.py b/jackify/frontends/gui/screens/install_ttw_workflow.py index 45a0221..dace1cd 100644 --- a/jackify/frontends/gui/screens/install_ttw_workflow.py +++ b/jackify/frontends/gui/screens/install_ttw_workflow.py @@ -209,6 +209,14 @@ class TTWWorkflowMixin: font-size: 13px; """) self._safe_append_text(f"\nError: {message}") + if getattr(self, '_ttw_unclean_game_dir_detected', False): + self._safe_append_text( + "\nLikely cause: Your Fallout New Vegas game directory is not clean vanilla.\n" + "TTW requires an unmodified FNV installation to patch correctly.\n" + "If you have previously installed an FNV modlist that modifies the game directory,\n" + "verify or reinstall FNV via Steam to restore vanilla files, then try again." + ) + self._last_install_message = message self.process_finished(1, QProcess.CrashExit) def process_finished(self, exit_code, exit_status): @@ -247,9 +255,11 @@ class TTWWorkflowMixin: ) else: last_output = self.console.toPlainText() - if "cancelled by user" in last_output.lower(): + failure_msg = (getattr(self, '_last_install_message', '') or "").strip() + if "cancelled by user" in last_output.lower() or "cancelled by user" in failure_msg.lower(): MessageService.information(self, "Installation Cancelled", "The installation was cancelled by the user.", safety_level="low") else: - MessageService.show_error(self, wabbajack_install_failed(f"Exit code {exit_code}. Check the console output for details.")) - self._safe_append_text(f"\nInstall failed (exit code {exit_code}).") + user_summary = failure_msg or "TTW installation failed. Review recent console output for the failing step." + MessageService.show_error(self, wabbajack_install_failed(user_summary)) + self._safe_append_text(f"\nInstall failed: {user_summary}") self.console.moveCursor(QTextCursor.End) diff --git a/jackify/frontends/gui/screens/screen_back_mixin.py b/jackify/frontends/gui/screens/screen_back_mixin.py index ce888c4..4fb0074 100644 --- a/jackify/frontends/gui/screens/screen_back_mixin.py +++ b/jackify/frontends/gui/screens/screen_back_mixin.py @@ -6,6 +6,7 @@ should use this mixin so the main window consistently collapses when leaving. """ from PySide6.QtCore import QSize, Qt +from PySide6.QtWidgets import QSizePolicy from ..utils import set_responsive_minimum @@ -48,3 +49,48 @@ class ScreenBackMixin: self.show_details_checkbox.blockSignals(False) if not is_steamdeck and hasattr(self, "_toggle_console_visibility"): self._toggle_console_visibility(Qt.Unchecked) + + def force_collapsed_details_state(self, resize_mode: str = "compact"): + """ + Normalize Show Details state when a screen is opened/reset. + + Some screens still manage console visibility locally instead of through a + single shared widget module. This helper forces the collapsed state in a + way that is safe across those implementations. + """ + try: + if hasattr(self, "show_details_checkbox"): + self.show_details_checkbox.blockSignals(True) + self.show_details_checkbox.setChecked(False) + self.show_details_checkbox.blockSignals(False) + except Exception: + pass + + try: + if hasattr(self, "console"): + self.console.setVisible(False) + self.console.setMinimumHeight(0) + self.console.setMaximumHeight(0) + self.console.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Fixed) + except Exception: + pass + + try: + if hasattr(self, "console_and_buttons_widget"): + self.console_and_buttons_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + self.console_and_buttons_widget.setFixedHeight(50) + self.console_and_buttons_widget.updateGeometry() + except Exception: + pass + + try: + if hasattr(self, "main_overall_vbox") and hasattr(self, "console"): + self.main_overall_vbox.setStretchFactor(self.console, 0) + except Exception: + pass + + try: + if hasattr(self, "resize_request"): + self.resize_request.emit(resize_mode) + except Exception: + pass diff --git a/jackify/frontends/gui/screens/screen_focus_reclaim.py b/jackify/frontends/gui/screens/screen_focus_reclaim.py new file mode 100644 index 0000000..31ca540 --- /dev/null +++ b/jackify/frontends/gui/screens/screen_focus_reclaim.py @@ -0,0 +1,58 @@ +"""Shared mixin for reclaiming window focus after Steam restart.""" +import logging +from PySide6.QtCore import QTimer, Qt + +logger = logging.getLogger(__name__) + +STEAM_RESTART_SENTINEL = "[Jackify] Steam restart complete" + + +class FocusReclaimMixin: + """Mixin providing post-Steam-restart focus reclaim for any screen. + + Usage: inherit this mixin and call _start_focus_reclaim_retries() when + Steam restart is detected. Detection is typically done by checking + progress messages for STEAM_RESTART_SENTINEL. + """ + + def _start_focus_reclaim_retries(self): + try: + if hasattr(self, "_focus_reclaim_timer") and self._focus_reclaim_timer: + self._focus_reclaim_timer.stop() + self._focus_reclaim_timer.deleteLater() + except Exception: + pass + + self._focus_reclaim_attempt = 0 + self._focus_reclaim_max_attempts = 12 # ~24 seconds total + self._focus_reclaim_timer = QTimer(self) + self._focus_reclaim_timer.setInterval(2000) + self._focus_reclaim_timer.timeout.connect(self._focus_reclaim_tick) + self._focus_reclaim_timer.start() + self._focus_reclaim_tick() + + def _focus_reclaim_tick(self): + try: + win = self.window() + if win is None: + return + + self._focus_reclaim_attempt += 1 + win.raise_() + win.activateWindow() + win.setWindowState(win.windowState() & ~Qt.WindowMinimized | Qt.WindowActive) + + if win.isActiveWindow(): + logger.info("Foreground focus reclaimed after Steam restart") + self._focus_reclaim_timer.stop() + return + + if self._focus_reclaim_attempt >= self._focus_reclaim_max_attempts: + logger.warning("Foreground focus reclaim timed out after Steam restart") + self._focus_reclaim_timer.stop() + except Exception as e: + logger.debug(f"Focus reclaim tick failed: {e}") + try: + self._focus_reclaim_timer.stop() + except Exception: + pass diff --git a/jackify/frontends/gui/screens/wabbajack_installer.py b/jackify/frontends/gui/screens/wabbajack_installer.py index 33fe25b..a65f8bc 100644 --- a/jackify/frontends/gui/screens/wabbajack_installer.py +++ b/jackify/frontends/gui/screens/wabbajack_installer.py @@ -20,9 +20,12 @@ from PySide6.QtCore import Qt, QThread, Signal, QSize from PySide6.QtGui import QTextCursor from jackify.backend.models.configuration import SystemInfo +from jackify.backend.services.automated_prefix_service import AutomatedPrefixService from jackify.shared.errors import wabbajack_install_failed +from ..dialogs.existing_setup_dialog import prompt_existing_setup_dialog from ..services.message_service import MessageService from ..shared_theme import JACKIFY_COLOR_BLUE, DEBUG_BORDERS +from .screen_focus_reclaim import FocusReclaimMixin, STEAM_RESTART_SENTINEL from ..utils import set_responsive_minimum from ..widgets.file_progress_list import FileProgressList from ..widgets.progress_indicator import OverallProgressIndicator @@ -39,11 +42,12 @@ class WabbajackInstallerWorker(QThread): log_output = Signal(str) # Console log output installation_complete = Signal(bool, str, str, str, str) # Success, message, launch_options, app_id, time_taken - def __init__(self, install_folder: Path, shortcut_name: str = "Wabbajack", enable_gog: bool = True): + def __init__(self, install_folder: Path, shortcut_name: str = "Wabbajack", enable_gog: bool = True, existing_appid: int | None = None): super().__init__() self.install_folder = install_folder self.shortcut_name = shortcut_name self.enable_gog = enable_gog + self.existing_appid = existing_appid self.launch_options = "" # Store launch options for success message self.start_time = None # Track installation start time @@ -73,6 +77,7 @@ class WabbajackInstallerWorker(QThread): install_folder=self.install_folder, shortcut_name=self.shortcut_name, enable_gog=self.enable_gog, + existing_appid=self.existing_appid, progress_callback=progress_callback, log_callback=log_callback ) @@ -84,7 +89,7 @@ class WabbajackInstallerWorker(QThread): self.installation_complete.emit(False, error_msg or "Installation failed", "", "", "") -class WabbajackInstallerScreen(ScreenBackMixin, QWidget): +class WabbajackInstallerScreen(ScreenBackMixin, FocusReclaimMixin, QWidget): """Wabbajack installer GUI screen following standard Jackify layout""" resize_request = Signal(str) @@ -347,10 +352,17 @@ class WabbajackInstallerScreen(ScreenBackMixin, QWidget): def _on_show_details_toggled(self, checked): """Handle Show details checkbox toggle""" - self.console.setVisible(checked) if checked: + self.console.setVisible(True) + self.console.setMinimumHeight(200) + self.console.setMaximumHeight(16777215) + self.console.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.resize_request.emit("expand") else: + self.console.setVisible(False) + self.console.setMinimumHeight(0) + self.console.setMaximumHeight(0) + self.console.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Fixed) self.resize_request.emit("compact") def _browse_folder(self): @@ -398,6 +410,51 @@ class WabbajackInstallerScreen(ScreenBackMixin, QWidget): if confirm != QMessageBox.Yes: return + existing_appid = None + candidate_exe = self.install_folder / "Wabbajack.exe" + prefix_service = AutomatedPrefixService() + conflict_result = prefix_service.handle_existing_shortcut_conflict( + self.shortcut_name, + str(candidate_exe), + str(self.install_folder), + ) + if isinstance(conflict_result, list): + action, new_name = prompt_existing_setup_dialog( + self, + window_title="Existing Modlist Setup Detected", + heading="Use Existing Setup or Create a New Shortcut", + body=( + "Jackify found an existing Steam shortcut for this Wabbajack setup.\n\n" + "Choose 'Use Existing Setup' to reuse the current Steam shortcut, or enter a " + "different name to create a separate shortcut." + ), + existing_name=conflict_result[0].get("name", self.shortcut_name), + requested_name=self.shortcut_name, + install_dir=str(self.install_folder), + field_label="New shortcut name", + reuse_label="Use Existing Setup", + new_label="Create New Shortcut", + cancel_label="Cancel", + ) + if action == "reuse": + existing_appid = conflict_result[0].get("appid") + if not existing_appid: + MessageService.warning(self, "Existing Setup Not Found", "Jackify could not determine the Steam AppID for the existing shortcut.") + return + self._write_to_log_file(f"Reusing existing Steam shortcut '{self.shortcut_name}' with AppID {existing_appid}") + elif action == "new": + if not new_name: + MessageService.warning(self, "Invalid Name", "Please enter a valid shortcut name.") + return + if new_name == self.shortcut_name: + MessageService.warning(self, "Same Name", "Please enter a different name to create a separate shortcut.") + return + self.shortcut_name = new_name + self.shortcut_name_edit.setText(new_name) + else: + self._write_to_log_file("Shortcut creation cancelled by user") + return + # Clear displays self.console.clear() self.file_progress_list.clear() @@ -420,7 +477,7 @@ class WabbajackInstallerScreen(ScreenBackMixin, QWidget): self.progress_indicator.set_status("Starting installation...", 0) # Start worker thread - self.worker = WabbajackInstallerWorker(self.install_folder, shortcut_name=self.shortcut_name, enable_gog=True) + self.worker = WabbajackInstallerWorker(self.install_folder, shortcut_name=self.shortcut_name, enable_gog=True, existing_appid=int(existing_appid) if existing_appid else None) self.worker.progress_update.connect(self._on_progress_update) self.worker.activity_update.connect(self._on_activity_update) self.worker.log_output.connect(self._on_log_output) @@ -428,8 +485,9 @@ class WabbajackInstallerScreen(ScreenBackMixin, QWidget): self.worker.start() def _on_progress_update(self, message: str, percentage: int): - """Handle progress updates""" self.progress_indicator.set_status(message, percentage) + if STEAM_RESTART_SENTINEL in message: + self._start_focus_reclaim_retries() def _on_activity_update(self, label: str, current: int, total: int): """Handle activity tab updates""" @@ -569,6 +627,7 @@ class WabbajackInstallerScreen(ScreenBackMixin, QWidget): def showEvent(self, event): """Called when widget becomes visible""" super().showEvent(event) + self.force_collapsed_details_state() try: main_window = self.window() if main_window: diff --git a/jackify/frontends/gui/services/vnv_automation_controller.py b/jackify/frontends/gui/services/vnv_automation_controller.py new file mode 100644 index 0000000..78e9d5f --- /dev/null +++ b/jackify/frontends/gui/services/vnv_automation_controller.py @@ -0,0 +1,402 @@ +""" +Shared VNV post-install automation controller for all GUI workflows. + +Handles VNV detection, user confirmation, premium/non-premium download paths, +worker thread management, and completion callbacks. +""" + +import logging +from pathlib import Path +from typing import Callable, Optional + +from PySide6.QtCore import QThread, Signal, Slot, QObject +from PySide6.QtWidgets import QMessageBox, QWidget + +logger = logging.getLogger(__name__) + + +class _VNVWorker(QThread): + """Background thread for VNV automation.""" + progress_update = Signal(str) + completed = Signal(bool, str) # (success, error_message) + + def __init__(self, modlist_name, install_path, game_root, ttw_installer_path): + super().__init__() + self._modlist_name = modlist_name + self._install_path = install_path + self._game_root = game_root + self._ttw_installer_path = ttw_installer_path + + def run(self): + try: + from jackify.backend.services.vnv_integration_helper import run_vnv_automation_if_applicable + automation_ran, error = run_vnv_automation_if_applicable( + modlist_name=self._modlist_name, + modlist_install_location=self._install_path, + game_root=self._game_root, + ttw_installer_path=self._ttw_installer_path, + progress_callback=self.progress_update.emit, + manual_file_callback=None, + confirmation_callback=lambda desc: True, + ) + self.completed.emit(error is None, error or "") + except Exception as e: + import traceback + self.completed.emit(False, f"{e}\n{traceback.format_exc()}") + + +class VNVAutomationController(QObject): + """ + Single entry point for VNV post-install automation across all GUI workflows. + + Usage in any screen's on_configuration_complete: + + from ..services.vnv_automation_controller import VNVAutomationController + controller = VNVAutomationController() + if controller.attempt( + parent=self, + modlist_name=modlist_name, + install_dir=install_dir, + on_progress=self._safe_append_text, + on_complete=lambda success, error: self._on_vnv_done(success, error), + ): + # VNV is running, defer success dialog + return + # No VNV, show success dialog now + """ + + # Emitted from the watcher background thread; delivered on main thread + # via auto-queued connection because this object lives on the main thread. + _worker_start_requested = Signal() + + def __init__(self): + super().__init__() + self._worker: Optional[_VNVWorker] = None + self._manual_manager = None + self._manual_dialog = None + self._pending_worker_start: Optional[Callable] = None + self._on_progress_cb: Optional[Callable] = None + self._on_complete_cb: Optional[Callable] = None + self._handle_feedback_cb: Optional[Callable] = None + self._worker_start_requested.connect(self._dispatch_worker_start) + + def attempt( + self, + parent: QWidget, + modlist_name: str, + install_dir: str, + on_progress: Callable[[str], None], + on_complete: Callable[[bool, str], None], + begin_feedback: Optional[Callable[[], None]] = None, + handle_feedback: Optional[Callable[[str], None]] = None, + ) -> bool: + """Check for VNV eligibility and start automation if applicable. + + Args: + parent: Parent QWidget for dialogs + modlist_name: Name of the modlist + install_dir: Installation directory path + on_progress: Called with progress text messages + on_complete: Called with (success, error_message) when done + begin_feedback: Optional - start post-install progress UI + handle_feedback: Optional - update post-install progress UI + + Returns: + True if VNV automation is starting (caller should defer success dialog) + False if no VNV needed (caller should show success dialog immediately) + """ + try: + from jackify.backend.services.vnv_integration_helper import should_offer_vnv_automation + from jackify.backend.handlers.path_handler import PathHandler + from jackify.backend.services.vnv_post_install_service import VNVPostInstallService + from jackify.backend.services.automated_prefix_service import AutomatedPrefixService + + install_path = Path(install_dir) + + if not should_offer_vnv_automation(modlist_name, install_path): + return False + + game_paths = PathHandler().find_vanilla_game_paths() + game_root = game_paths.get('Fallout New Vegas') + if not game_root: + logger.debug("VNV automation skipped - FNV game root not found") + on_progress("VNV automation skipped: Fallout New Vegas path not found") + return False + + # Check completion status + vnv_service = VNVPostInstallService( + modlist_install_location=install_path, + game_root=game_root, + ttw_installer_path=AutomatedPrefixService.get_ttw_installer_path(), + ) + completed = vnv_service.check_already_completed() + if completed['root_mods'] and completed['4gb_patch'] and completed['bsa_decompressed']: + logger.info("VNV automation steps already completed") + return False + + # Confirmation dialog + from .message_service import MessageService + reply = MessageService.question( + parent, + "VNV Post-Install Automation", + vnv_service.get_automation_description(), + critical=False, + safety_level="medium", + ) + if reply != QMessageBox.Yes: + logger.info("User declined VNV automation") + on_progress("VNV automation skipped by user") + return False + + ttw_installer_path = AutomatedPrefixService.get_ttw_installer_path() + + # Non-premium path: route 4GB patcher through ManualDownloadManager + from jackify.backend.services.nexus_auth_service import NexusAuthService + from jackify.backend.services.nexus_premium_service import NexusPremiumService + + auth_svc = NexusAuthService() + token = auth_svc.get_auth_token() + is_premium = False + if token: + is_premium, _ = NexusPremiumService().check_premium_status( + token, is_oauth=(auth_svc.get_auth_method() == "oauth") + ) + + if not is_premium: + has_4gb_cache = vnv_service._find_cached_4gb_patcher() is not None + has_bsa_cache = ( + vnv_service._find_cached_bsa_mpi() is not None or + vnv_service._find_cached_bsa_package() is not None + ) + if has_4gb_cache and has_bsa_cache: + logger.debug("VNV non-premium: required VNV tools already cached, proceeding to worker") + else: + tool_events = vnv_service.get_manual_download_items(include_bsa=not has_bsa_cache) + logger.debug("VNV non-premium: tool_events=%d, cache_dir=%s", len(tool_events), vnv_service.cache_dir) + if tool_events: + if begin_feedback: + begin_feedback() + self._show_tool_download_dialog( + parent, tool_events, vnv_service.cache_dir, + modlist_name, install_path, game_root, ttw_installer_path, + on_progress, on_complete, handle_feedback, + ) + return True + else: + # Nexus API unavailable — can't auto-track the download. + # Open the mod page so the user can get it manually and inform + # them where to place it so the worker finds it next time. + logger.warning("VNV non-premium: Nexus API query failed, cannot open download manager") + try: + import subprocess + subprocess.Popen(['xdg-open', 'https://www.nexusmods.com/newvegas/mods/62552?tab=files']) + except Exception: + pass + from .message_service import MessageService + MessageService.information( + parent, + "VNV Tools — Manual Download Required", + "Jackify could not query the Nexus download URL(s) (check your Nexus login in Settings).\n\n" + "Your modlist has been installed successfully.\n\n" + "To complete VNV post-install setup, please:\n" + "1. Download the '4GB Patcher (Linux/Proton)' from:\n" + " nexusmods.com/newvegas/mods/62552\n\n" + "2. Download the BSA Decompressor package from:\n" + " nexusmods.com/newvegas/mods/65854\n\n" + f"3. Place the archive(s) in:\n {vnv_service.cache_dir}\n\n" + "4. Re-configure the modlist — Jackify will detect the files automatically.", + ) + return False + + # Premium or all tools already cached - start worker directly + if begin_feedback: + begin_feedback() + self._start_worker( + parent, modlist_name, install_path, game_root, + ttw_installer_path, on_progress, on_complete, handle_feedback, + ) + return True + + except Exception as e: + logger.error("Failed to start VNV automation: %s", e) + import traceback + logger.error("Traceback: %s", traceback.format_exc()) + return False + + def _dispatch_worker_start(self): + """Slot — always runs on the main thread due to queued signal delivery.""" + if self._pending_worker_start: + fn = self._pending_worker_start + self._pending_worker_start = None + fn() + + def _show_tool_download_dialog( + self, parent, tool_events, cache_dir, + modlist_name, install_path, game_root, ttw_installer_path, + on_progress, on_complete, handle_feedback, + ): + """Show ManualDownloadDialog for VNV tools that need manual download.""" + from jackify.backend.services.manual_download_manager import ManualDownloadManager + from jackify.frontends.gui.dialogs.manual_download_dialog import ManualDownloadDialog + from jackify.backend.handlers.config_handler import ConfigHandler + + cfg_watch = ConfigHandler().get("manual_download_watch_directory", None) + watch_dir = None + if cfg_watch: + p = Path(str(cfg_watch)).expanduser() + if p.is_dir(): + watch_dir = p + if watch_dir is None: + import os + xdg = os.environ.get('XDG_DOWNLOAD_DIR', '') + xdg_path = Path(xdg).expanduser() if xdg else None + watch_dir = xdg_path if (xdg_path and xdg_path.is_dir()) else Path.home() / 'Downloads' + + def _on_all_done(_completed, _skipped): + # _check_all_done() runs in the watcher background thread (Python + # threading.Thread — no Qt event loop). QTimer.singleShot is + # unreliable from non-Qt threads. Instead, emit a signal: because + # VNVAutomationController was created on the main thread, Qt uses a + # queued connection automatically and delivers the slot on the main thread. + self._pending_worker_start = lambda: self._finish_manual_download_flow( + state, + parent, + modlist_name, + install_path, + game_root, + ttw_installer_path, + on_progress, + on_complete, + handle_feedback, + ) + self._worker_start_requested.emit() + + state = {"done": False} + + manager = ManualDownloadManager( + modlist_download_dir=cache_dir, + watch_directory=watch_dir, + concurrent_limit=2, + on_all_done=_on_all_done, + ) + self._manual_manager = manager + manager.load_items(tool_events, loop_iteration=1) + + dialog = ManualDownloadDialog( + manager=manager, + modlist_name="VNV Post-Install Tools", + watch_directory=watch_dir, + concurrent_limit=2, + parent=parent, + ) + self._manual_dialog = dialog + dialog.load_items(manager.items) + dialog.finished.connect(lambda _result: self._cancel_manual_download_flow(on_complete, state)) + dialog.show() + + def _cancel_manual_download_flow(self, on_complete, state: dict) -> None: + if state["done"]: + return + state["done"] = True + self._stop_manual_download_flow() + on_complete(False, "") + + def _finish_manual_download_flow( + self, + state: dict, + parent, + modlist_name, + install_path, + game_root, + ttw_installer_path, + on_progress, + on_complete, + handle_feedback, + ) -> None: + if state["done"]: + return + state["done"] = True + self._stop_manual_download_flow() + self._start_worker( + parent, + modlist_name, + install_path, + game_root, + ttw_installer_path, + on_progress, + on_complete, + handle_feedback, + ) + + def _stop_manual_download_flow(self) -> None: + dialog = self._manual_dialog + manager = self._manual_manager + self._manual_dialog = None + self._manual_manager = None + if dialog is not None: + try: + dialog.finished.disconnect() + except Exception: + pass + try: + dialog.close() + except Exception: + pass + if manager is not None: + try: + manager.stop() + except Exception: + pass + + def _start_worker( + self, parent, modlist_name, install_path, game_root, + ttw_installer_path, on_progress, on_complete, handle_feedback, + ): + """Create and start VNV worker thread. + + Signals are connected to @Slot methods on this QObject (main thread). + Because VNVAutomationController lives on the main thread, Qt automatically + uses queued connections for signals emitted from the worker thread, + guaranteeing that _on_worker_progress and _on_worker_done execute on + the main thread regardless of which thread the worker emits from. + """ + self._on_progress_cb = on_progress + self._on_complete_cb = on_complete + self._handle_feedback_cb = handle_feedback + + self._worker = _VNVWorker( + modlist_name, install_path, game_root, ttw_installer_path, + ) + self._worker.progress_update.connect(self._on_worker_progress) + self._worker.completed.connect(self._on_worker_done) + self._worker.finished.connect(self._worker.deleteLater) + self._worker.start() + + @Slot(str) + def _on_worker_progress(self, message: str): + if self._on_progress_cb: + self._on_progress_cb(message) + if self._handle_feedback_cb: + self._handle_feedback_cb(message) + + @Slot(bool, str) + def _on_worker_done(self, success: bool, error: str): + self._worker = None + cb = self._on_complete_cb + self._on_complete_cb = None + self._on_progress_cb = None + self._handle_feedback_cb = None + if cb: + cb(success, error) + + def cleanup(self): + """Stop worker if running. Call from screen cleanup/hideEvent.""" + self._on_complete_cb = None + self._on_progress_cb = None + self._handle_feedback_cb = None + self._pending_worker_start = None + self._stop_manual_download_flow() + if self._worker and self._worker.isRunning(): + self._worker.terminate() + self._worker.wait(2000) + self._worker = None diff --git a/jackify/frontends/gui/widgets/file_progress_list.py b/jackify/frontends/gui/widgets/file_progress_list.py index bb31204..064ff58 100644 --- a/jackify/frontends/gui/widgets/file_progress_list.py +++ b/jackify/frontends/gui/widgets/file_progress_list.py @@ -342,7 +342,9 @@ class FileProgressList(QWidget): self._cpu_timer.stop() if self._cpu_worker and self._cpu_worker.isRunning(): self._cpu_worker.quit() - self._cpu_worker.wait(500) + if not self._cpu_worker.wait(500): + self._cpu_worker.terminate() + self._cpu_worker.wait(1000) self._cpu_worker = None def _start_cpu_worker(self): diff --git a/jackify/shared/appimage_utils.py b/jackify/shared/appimage_utils.py index e95c191..0596801 100644 --- a/jackify/shared/appimage_utils.py +++ b/jackify/shared/appimage_utils.py @@ -45,7 +45,7 @@ def get_appimage_path() -> Optional[Path]: if 'jackify' in path.name.lower(): return path else: - # Running from different AppImage (e.g., development in Cursor.AppImage) + # Running from a different AppImage context return None return None @@ -94,4 +94,4 @@ def get_appimage_info() -> dict: except (OSError, PermissionError): pass - return info \ No newline at end of file + return info diff --git a/jackify/shared/errors.py b/jackify/shared/errors.py index 36ae1ea..767d67f 100644 --- a/jackify/shared/errors.py +++ b/jackify/shared/errors.py @@ -75,12 +75,24 @@ def _looks_sensitive_key(key: str) -> bool: def _scrub_sensitive_text(text: str) -> str: """Best-effort redaction for key=value style sensitive fragments.""" scrubbed = text - patterns = [ + # Authorization header forms: "authorization: Bearer " + scrubbed = re.sub( + r"(?i)\bauthorization\b\s*[:=]\s*bearer\s+[A-Za-z0-9\-._~+/]+=*", + "authorization=[REDACTED]", + scrubbed, + ) + # Standalone bearer form: "Bearer " + scrubbed = re.sub( + r"(?i)\b(bearer)\s+[A-Za-z0-9\-._~+/]+=*", + r"\1=[REDACTED]", + scrubbed, + ) + # Generic sensitive key/value forms. + scrubbed = re.sub( r"(?i)\b(api[_-]?key|access[_-]?token|refresh[_-]?token|token|authorization|password|secret)\b\s*[:=]\s*([^\s,;]+)", - r"(?i)\b(bearer)\s+([A-Za-z0-9\-._~+/]+=*)", - ] - for pattern in patterns: - scrubbed = re.sub(pattern, r"\1=[REDACTED]", scrubbed) + r"\1=[REDACTED]", + scrubbed, + ) return scrubbed @@ -226,7 +238,24 @@ def modlist_not_found(path: str) -> ModlistError: ) -def configuration_failed(detail: str) -> ConfigError: +def game_not_found_for_modlist(game_name: str, detail: Optional[str] = None) -> InstallError: + game = (game_name or "Unknown game").strip() + return InstallError( + title="Required Game Not Found", + message=f"Jackify could not find the required base game: {game}", + suggestion="Install the base game in Steam, launch it once, then retry.", + solutions=[ + "Confirm the game is installed in Steam and fully updated.", + "Launch the vanilla game once from Steam to complete first-run setup.", + "If you have multiple Steam installs, ensure Jackify is pointed at the install that contains this game.", + "Restart Steam and retry the install workflow.", + f"If detection still fails, check Jackify logs ({_logs_dir_display()}) for game-detection details.", + ], + technical=format_technical_context(detail=detail, context={"required_game": game}), + ) + + +def configuration_failed(detail: str, context: Optional[dict] = None) -> ConfigError: return ConfigError( title="Post-Install Configuration Failed", message="Jackify could not complete the post-installation configuration for this modlist.", @@ -239,7 +268,7 @@ def configuration_failed(detail: str) -> ConfigError: "If the error mentions registry or prefix, ensure sufficient disk space.", f"If this still fails, check Jackify logs ({_logs_dir_display()}) and open a GitHub issue with modlist name.", ], - technical=format_technical_context(detail=detail), + technical=format_technical_context(detail=detail, context=context), ) @@ -261,7 +290,7 @@ def ttw_install_failed(detail: str) -> TTWError: ) -def wabbajack_install_failed(detail: str) -> InstallError: +def wabbajack_install_failed(detail: str, context: Optional[dict] = None) -> InstallError: return InstallError( title="Wabbajack Installation Failed", message="The modlist installation did not complete successfully.", @@ -275,7 +304,7 @@ def wabbajack_install_failed(detail: str) -> InstallError: "Check Modlist_Install_workflow.log for the specific file that failed.", "If the same failure repeats with no clear workaround, open a GitHub issue with logs.", ], - technical=format_technical_context(detail=detail), + technical=format_technical_context(detail=detail, context=context), ) @@ -324,6 +353,27 @@ def manual_steps_incomplete() -> ConfigError: ) +def cc_content_missing(filename: str = "") -> InstallError: + detail = f"Missing file: {filename}" if filename else "" + return InstallError( + title="Anniversary Edition Content Missing", + message=( + "One or more Skyrim Anniversary Edition Creation Club files were not found " + "in your game installation." + + (f" ({filename})" if filename else "") + ), + suggestion="Open Vanilla Skyrim and allow it to download the required Anniversary Edition content.", + solutions=[ + "Open Vanilla Skyrim SE/AE and let it run until all Creation Club content has downloaded.", + "From the Skyrim main menu, go into Creations and select 'Download All'.", + "If specific files are still missing, search for and download them individually from the Creations menu.", + "If problems persist, uninstall and reinstall Skyrim, then launch once to trigger the AE download.", + "Note: Skyrim AE via Steam Family Sharing does not transfer DLC content — you must own AE directly.", + ], + technical=format_technical_context(detail=detail) if detail else None, + ) + + def mo2_setup_failed(detail: str) -> InstallError: return InstallError( title="Mod Organizer 2 Setup Failed", diff --git a/jackify/shared/steam_utils.py b/jackify/shared/steam_utils.py index 3b45ba6..c88616b 100644 --- a/jackify/shared/steam_utils.py +++ b/jackify/shared/steam_utils.py @@ -5,12 +5,41 @@ Centralized Steam installation type detection to avoid redundant subprocess call """ import logging +import os +import re import subprocess import shutil -from typing import Tuple +from pathlib import Path +from typing import Dict, List, Optional, Tuple logger = logging.getLogger(__name__) +NATIVE_STEAM_ROOTS = [ + Path.home() / ".steam" / "steam", + Path.home() / ".local" / "share" / "Steam", + Path.home() / ".steam" / "root", +] + +FLATPAK_STEAM_ROOTS = [ + Path.home() / ".var" / "app" / "com.valvesoftware.Steam" / "data" / "Steam", + Path.home() / ".var" / "app" / "com.valvesoftware.Steam" / ".local" / "share" / "Steam", + Path.home() / ".var" / "app" / "com.valvesoftware.Steam" / "home" / ".local" / "share" / "Steam", +] + +STEAM_PREFERENCE_AUTO = "auto" +STEAM_PREFERENCE_NATIVE = "native" +STEAM_PREFERENCE_FLATPAK = "flatpak" + +# Common Jackify-supported game AppIDs used to infer which Steam install is actually in use. +_STEAM_USAGE_APPIDS = { + "489830", # Skyrim Special Edition + "377160", # Fallout 4 + "22380", # Fallout New Vegas + "22330", # Oblivion + "22370", # Fallout 3 + "1716740", # Starfield +} + def detect_steam_installation_types() -> Tuple[bool, bool]: """ @@ -21,14 +50,187 @@ def detect_steam_installation_types() -> Tuple[bool, bool]: Returns: Tuple[bool, bool]: (is_flatpak_steam, is_native_steam) """ - is_flatpak = _detect_flatpak_steam() - is_native = _detect_native_steam() + raw_flatpak = _detect_flatpak_steam() + raw_native = _detect_native_steam() - logger.info(f"Steam installation detection: Flatpak={is_flatpak}, Native={is_native}") + is_flatpak = raw_flatpak + is_native = raw_native + preferred_type, preferred_root = resolve_preferred_steam_installation() + + # Deterministic dual-install behavior: expose one active Steam type. + if raw_flatpak and raw_native: + if preferred_type == STEAM_PREFERENCE_FLATPAK: + is_flatpak, is_native = True, False + else: + is_flatpak, is_native = False, True + + logger.info( + "Steam installation detection: Flatpak=%s, Native=%s, Preferred=%s (%s), RawFlatpak=%s, RawNative=%s", + is_flatpak, + is_native, + preferred_type or "none", + preferred_root or "n/a", + raw_flatpak, + raw_native, + ) return is_flatpak, is_native +def get_steam_install_roots(install_type: Optional[str] = None) -> List[Path]: + """Return known Steam roots for a specific install type or both.""" + if install_type == STEAM_PREFERENCE_FLATPAK: + return list(FLATPAK_STEAM_ROOTS) + if install_type == STEAM_PREFERENCE_NATIVE: + return list(NATIVE_STEAM_ROOTS) + return list(NATIVE_STEAM_ROOTS) + list(FLATPAK_STEAM_ROOTS) + + +def is_flatpak_steam_root(path: Path) -> bool: + """Return True if a Steam root path belongs to Flatpak Steam.""" + path_str = str(path) + return ".var/app/com.valvesoftware.Steam" in path_str + + +def get_available_steam_roots() -> Dict[str, List[Path]]: + """Return discovered Steam roots grouped by install type.""" + roots = { + STEAM_PREFERENCE_NATIVE: [], + STEAM_PREFERENCE_FLATPAK: [], + } + for root in NATIVE_STEAM_ROOTS: + if root.exists(): + roots[STEAM_PREFERENCE_NATIVE].append(root) + for root in FLATPAK_STEAM_ROOTS: + if root.exists(): + roots[STEAM_PREFERENCE_FLATPAK].append(root) + return roots + + +def get_ordered_steam_roots(preference: str = STEAM_PREFERENCE_AUTO) -> List[Path]: + """ + Return Steam roots in deterministic priority order. + + If both native and flatpak are installed, preference controls order. + AUTO uses the most recently active install (loginusers.vdf timestamp/mtime). + """ + available = get_available_steam_roots() + native_roots = available[STEAM_PREFERENCE_NATIVE] + flatpak_roots = available[STEAM_PREFERENCE_FLATPAK] + + if preference not in { + STEAM_PREFERENCE_AUTO, + STEAM_PREFERENCE_NATIVE, + STEAM_PREFERENCE_FLATPAK, + }: + preference = STEAM_PREFERENCE_AUTO + + if preference == STEAM_PREFERENCE_NATIVE: + return native_roots + flatpak_roots + if preference == STEAM_PREFERENCE_FLATPAK: + return flatpak_roots + native_roots + + preferred_type, _ = resolve_preferred_steam_installation(STEAM_PREFERENCE_AUTO) + if preferred_type == STEAM_PREFERENCE_FLATPAK: + return flatpak_roots + native_roots + return native_roots + flatpak_roots + + +def resolve_preferred_steam_installation( + preference: str = STEAM_PREFERENCE_AUTO, +) -> Tuple[Optional[str], Optional[Path]]: + """ + Resolve the preferred Steam install type/root deterministically. + + Priority: + 1) Explicit preference (`native` or `flatpak`) if installed + 2) AUTO mode: whichever install has more relevant installed-game manifests + 3) AUTO tie-break: newest loginusers activity marker + 4) Deterministic fallback: native first, then flatpak + """ + available = get_available_steam_roots() + native_roots = available[STEAM_PREFERENCE_NATIVE] + flatpak_roots = available[STEAM_PREFERENCE_FLATPAK] + + if preference == STEAM_PREFERENCE_NATIVE and native_roots: + return STEAM_PREFERENCE_NATIVE, native_roots[0] + if preference == STEAM_PREFERENCE_FLATPAK and flatpak_roots: + return STEAM_PREFERENCE_FLATPAK, flatpak_roots[0] + + if native_roots and flatpak_roots: + native_game_score = _steam_root_game_presence_score(native_roots[0]) + flatpak_game_score = _steam_root_game_presence_score(flatpak_roots[0]) + if flatpak_game_score > native_game_score: + return STEAM_PREFERENCE_FLATPAK, flatpak_roots[0] + if native_game_score > flatpak_game_score: + return STEAM_PREFERENCE_NATIVE, native_roots[0] + + native_score = _steam_root_activity_score(native_roots[0]) + flatpak_score = _steam_root_activity_score(flatpak_roots[0]) + if flatpak_score > native_score: + return STEAM_PREFERENCE_FLATPAK, flatpak_roots[0] + return STEAM_PREFERENCE_NATIVE, native_roots[0] + + if native_roots: + return STEAM_PREFERENCE_NATIVE, native_roots[0] + if flatpak_roots: + return STEAM_PREFERENCE_FLATPAK, flatpak_roots[0] + return None, None + + +def _steam_root_activity_score(steam_root: Path) -> float: + """ + Return a comparable activity score for Steam root. + Uses loginusers.vdf mtime as a robust cross-layout signal. + """ + try: + loginusers = steam_root / "config" / "loginusers.vdf" + if loginusers.exists(): + return os.path.getmtime(loginusers) + except Exception as exc: + logger.debug("Could not read Steam activity marker for %s: %s", steam_root, exc) + return 0.0 + + +def _steam_root_game_presence_score(steam_root: Path) -> int: + """ + Score a Steam root by presence of relevant installed game appmanifests. + Higher score means that Steam install is more likely the one user is actively using. + """ + score = 0 + for library_root in _get_library_roots_for_steam_root(steam_root): + steamapps = library_root / "steamapps" + if not steamapps.is_dir(): + continue + for app_id in _STEAM_USAGE_APPIDS: + manifest = steamapps / f"appmanifest_{app_id}.acf" + if manifest.is_file(): + score += 1 + return score + + +def _get_library_roots_for_steam_root(steam_root: Path) -> List[Path]: + """ + Return Steam library roots for a given Steam root using libraryfolders.vdf. + Includes the primary Steam root as a fallback. + """ + roots: List[Path] = [steam_root] + vdf_path = steam_root / "config" / "libraryfolders.vdf" + if not vdf_path.is_file(): + return roots + + try: + text = vdf_path.read_text(encoding="utf-8", errors="ignore") + for match in re.finditer(r'"path"\s*"([^"]+)"', text): + raw_path = match.group(1).replace("\\\\", "\\") + lib_root = Path(raw_path).expanduser() + if lib_root not in roots: + roots.append(lib_root) + except Exception as exc: + logger.debug("Failed reading %s: %s", vdf_path, exc) + return roots + + def _detect_flatpak_steam() -> bool: """Detect if Steam is installed as a Flatpak.""" try: @@ -58,16 +260,8 @@ def _detect_flatpak_steam() -> bool: def _detect_native_steam() -> bool: """Detect if native Steam installation exists.""" try: - # Check for common Steam paths - import os - steam_paths = [ - os.path.expanduser("~/.steam/steam"), - os.path.expanduser("~/.local/share/Steam"), - os.path.expanduser("~/.steam/root") - ] - - for path in steam_paths: - if os.path.exists(path): + for path in NATIVE_STEAM_ROOTS: + if path.exists(): logger.debug(f"Native Steam detected at: {path}") return True diff --git a/requirements.txt b/requirements.txt index 588268a..ac154a4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -31,4 +31,7 @@ packaging>=21.0 # Time handling (stdlib: time, datetime) # Collections (stdlib: collections, itertools, functools) # Configuration files (stdlib: configparser) -# Hashing (stdlib: hashlib) \ No newline at end of file +# Hashing (stdlib: hashlib) + +# Non-premium download support +watchdog>=3.0.0 \ No newline at end of file