mirror of
https://github.com/Omni-guides/Jackify.git
synced 2026-01-17 19:47:00 +01:00
Sync from development - prepare for v0.2.1
This commit is contained in:
317
jackify/backend/handlers/enb_handler.py
Normal file
317
jackify/backend/handlers/enb_handler.py
Normal file
@@ -0,0 +1,317 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
ENB Handler Module
|
||||
Handles ENB detection and Linux compatibility configuration for modlists.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import configparser
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, Optional, Tuple
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ENBHandler:
|
||||
"""
|
||||
Handles ENB detection and configuration for Linux compatibility.
|
||||
|
||||
Detects ENB components in modlist installations and ensures enblocal.ini
|
||||
has the required LinuxVersion=true setting in the [GLOBAL] section.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize ENB handler."""
|
||||
self.logger = logger
|
||||
|
||||
def detect_enb_in_modlist(self, modlist_path: Path) -> Dict[str, Any]:
|
||||
"""
|
||||
Detect ENB components in modlist installation.
|
||||
|
||||
Searches for ENB configuration files:
|
||||
- enbseries.ini, enblocal.ini (ENB configuration files)
|
||||
|
||||
Note: Does NOT check for DLL files (d3d9.dll, d3d11.dll, dxgi.dll) as these
|
||||
are used by many other mods (ReShade, other graphics mods) and are not
|
||||
reliable indicators of ENB presence.
|
||||
|
||||
Args:
|
||||
modlist_path: Path to modlist installation directory
|
||||
|
||||
Returns:
|
||||
Dict with detection results:
|
||||
- has_enb: bool - True if ENB config files found
|
||||
- enblocal_ini: str or None - Path to enblocal.ini if found
|
||||
- enbseries_ini: str or None - Path to enbseries.ini if found
|
||||
- d3d9_dll: str or None - Always None (not checked)
|
||||
- d3d11_dll: str or None - Always None (not checked)
|
||||
- dxgi_dll: str or None - Always None (not checked)
|
||||
"""
|
||||
enb_info = {
|
||||
'has_enb': False,
|
||||
'enblocal_ini': None,
|
||||
'enbseries_ini': None,
|
||||
'd3d9_dll': None,
|
||||
'd3d11_dll': None,
|
||||
'dxgi_dll': None
|
||||
}
|
||||
|
||||
if not modlist_path.exists():
|
||||
self.logger.warning(f"Modlist path does not exist: {modlist_path}")
|
||||
return enb_info
|
||||
|
||||
# Search for ENB indicator files
|
||||
# IMPORTANT: Only check for ENB config files (enbseries.ini, enblocal.ini)
|
||||
# Do NOT check for DLL files (d3d9.dll, d3d11.dll, dxgi.dll) as these are used
|
||||
# by many other mods (ReShade, other graphics mods) and are not reliable ENB indicators
|
||||
|
||||
enb_config_patterns = [
|
||||
('**/enbseries.ini', 'enbseries_ini'),
|
||||
('**/enblocal.ini', 'enblocal_ini')
|
||||
]
|
||||
|
||||
for pattern, key in enb_config_patterns:
|
||||
for file_path in modlist_path.glob(pattern):
|
||||
# Skip backups and plugin data directories
|
||||
if "Backup" in str(file_path) or "plugins/data" in str(file_path):
|
||||
continue
|
||||
|
||||
enb_info['has_enb'] = True
|
||||
if not enb_info[key]: # Store first match
|
||||
enb_info[key] = str(file_path)
|
||||
|
||||
# If we detected ENB config but didn't find enblocal.ini via glob,
|
||||
# use the priority-based finder
|
||||
if enb_info['has_enb'] and not enb_info['enblocal_ini']:
|
||||
found_ini = self.find_enblocal_ini(modlist_path)
|
||||
if found_ini:
|
||||
enb_info['enblocal_ini'] = str(found_ini)
|
||||
|
||||
return enb_info
|
||||
|
||||
def find_enblocal_ini(self, modlist_path: Path) -> Optional[Path]:
|
||||
"""
|
||||
Find enblocal.ini in modlist installation using priority-based search.
|
||||
|
||||
Search order (highest priority first):
|
||||
1. Stock Game/Game Root directories (active locations)
|
||||
2. Mods folder with Root/root subfolder (most common pattern)
|
||||
3. Direct in mods/fixes folders
|
||||
4. Fallback recursive search (excluding backups)
|
||||
|
||||
Args:
|
||||
modlist_path: Path to modlist installation directory
|
||||
|
||||
Returns:
|
||||
Path to enblocal.ini if found, None otherwise
|
||||
"""
|
||||
if not modlist_path.exists():
|
||||
return None
|
||||
|
||||
# Priority 1: Stock Game/Game Root (active locations)
|
||||
stock_game_names = [
|
||||
"Stock Game",
|
||||
"Game Root",
|
||||
"STOCK GAME",
|
||||
"Stock Game Folder",
|
||||
"Stock Folder",
|
||||
"Skyrim Stock"
|
||||
]
|
||||
|
||||
for name in stock_game_names:
|
||||
candidate = modlist_path / name / "enblocal.ini"
|
||||
if candidate.exists():
|
||||
self.logger.debug(f"Found enblocal.ini in Stock Game location: {candidate}")
|
||||
return candidate
|
||||
|
||||
# Priority 2: Mods folder with Root/root subfolder
|
||||
mods_dir = modlist_path / "mods"
|
||||
if mods_dir.exists():
|
||||
# Search for Root/root subfolders
|
||||
for root_dir in mods_dir.rglob("Root"):
|
||||
candidate = root_dir / "enblocal.ini"
|
||||
if candidate.exists():
|
||||
self.logger.debug(f"Found enblocal.ini in mods/Root: {candidate}")
|
||||
return candidate
|
||||
|
||||
for root_dir in mods_dir.rglob("root"):
|
||||
candidate = root_dir / "enblocal.ini"
|
||||
if candidate.exists():
|
||||
self.logger.debug(f"Found enblocal.ini in mods/root: {candidate}")
|
||||
return candidate
|
||||
|
||||
# Priority 3: Direct in mods/fixes folders
|
||||
for search_dir in [modlist_path / "mods", modlist_path / "fixes"]:
|
||||
if search_dir.exists():
|
||||
for enb_file in search_dir.rglob("enblocal.ini"):
|
||||
# Skip backups and plugin data
|
||||
if "Backup" not in str(enb_file) and "plugins/data" not in str(enb_file):
|
||||
self.logger.debug(f"Found enblocal.ini in {search_dir.name}: {enb_file}")
|
||||
return enb_file
|
||||
|
||||
# Priority 4: Fallback recursive search (exclude backups)
|
||||
for enb_file in modlist_path.rglob("enblocal.ini"):
|
||||
if "Backup" not in str(enb_file) and "plugins/data" not in str(enb_file):
|
||||
self.logger.debug(f"Found enblocal.ini via recursive search: {enb_file}")
|
||||
return enb_file
|
||||
|
||||
return None
|
||||
|
||||
def ensure_linux_version_setting(self, enblocal_ini_path: Path) -> bool:
|
||||
"""
|
||||
Safely ensure [GLOBAL] section exists with LinuxVersion=true in enblocal.ini.
|
||||
|
||||
Safety features:
|
||||
- Verifies file exists before attempting modification
|
||||
- Checks if [GLOBAL] section exists before adding (prevents duplicates)
|
||||
- Creates backup before any write operation
|
||||
- Only writes if changes are actually needed
|
||||
- Handles encoding issues gracefully
|
||||
- Preserves existing file structure and comments
|
||||
|
||||
Args:
|
||||
enblocal_ini_path: Path to enblocal.ini file
|
||||
|
||||
Returns:
|
||||
bool: True if successful or no changes needed, False on error
|
||||
"""
|
||||
try:
|
||||
# Safety check: file must exist
|
||||
if not enblocal_ini_path.exists():
|
||||
self.logger.warning(f"enblocal.ini not found at: {enblocal_ini_path}")
|
||||
return False
|
||||
|
||||
# Read existing INI with same settings as modlist_handler.py
|
||||
config = configparser.ConfigParser(
|
||||
allow_no_value=True,
|
||||
delimiters=['=']
|
||||
)
|
||||
config.optionxform = str # Preserve case sensitivity
|
||||
|
||||
# Read with encoding handling (same pattern as modlist_handler.py)
|
||||
try:
|
||||
with open(enblocal_ini_path, 'r', encoding='utf-8-sig') as f:
|
||||
config.read_file(f)
|
||||
except UnicodeDecodeError:
|
||||
with open(enblocal_ini_path, 'r', encoding='latin-1') as f:
|
||||
config.read_file(f)
|
||||
except configparser.DuplicateSectionError as e:
|
||||
# If file has duplicate [GLOBAL] sections, log warning and skip
|
||||
self.logger.warning(f"enblocal.ini has duplicate sections: {e}. Skipping modification.")
|
||||
return False
|
||||
|
||||
# Check if [GLOBAL] section exists (case-insensitive check)
|
||||
global_section_exists = False
|
||||
global_section_name = None
|
||||
|
||||
# Find existing [GLOBAL] section (case-insensitive)
|
||||
for section_name in config.sections():
|
||||
if section_name.upper() == 'GLOBAL':
|
||||
global_section_exists = True
|
||||
global_section_name = section_name # Use actual case
|
||||
break
|
||||
|
||||
# Check current LinuxVersion value
|
||||
needs_update = False
|
||||
if global_section_exists:
|
||||
# Section exists - check if LinuxVersion needs updating
|
||||
current_value = config.get(global_section_name, 'LinuxVersion', fallback=None)
|
||||
if current_value is None or current_value.lower() != 'true':
|
||||
needs_update = True
|
||||
else:
|
||||
# Section doesn't exist - we need to add it
|
||||
needs_update = True
|
||||
|
||||
# If no changes needed, return success
|
||||
if not needs_update:
|
||||
self.logger.debug(f"enblocal.ini already has LinuxVersion=true in [GLOBAL] section")
|
||||
return True
|
||||
|
||||
# Changes needed - create backup first
|
||||
backup_path = enblocal_ini_path.with_suffix('.ini.jackify_backup')
|
||||
try:
|
||||
if not backup_path.exists():
|
||||
shutil.copy2(enblocal_ini_path, backup_path)
|
||||
self.logger.debug(f"Created backup: {backup_path}")
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Failed to create backup: {e}. Proceeding anyway.")
|
||||
|
||||
# Make changes
|
||||
if not global_section_exists:
|
||||
# Add [GLOBAL] section (configparser will use exact case 'GLOBAL')
|
||||
config.add_section('GLOBAL')
|
||||
global_section_name = 'GLOBAL'
|
||||
self.logger.debug("Added [GLOBAL] section to enblocal.ini")
|
||||
|
||||
# Set LinuxVersion=true
|
||||
config.set(global_section_name, 'LinuxVersion', 'true')
|
||||
self.logger.debug(f"Set LinuxVersion=true in [GLOBAL] section")
|
||||
|
||||
# Write back to file
|
||||
with open(enblocal_ini_path, 'w', encoding='utf-8') as f:
|
||||
config.write(f, space_around_delimiters=False)
|
||||
|
||||
self.logger.info(f"Successfully configured enblocal.ini: {enblocal_ini_path}")
|
||||
return True
|
||||
|
||||
except configparser.DuplicateSectionError as e:
|
||||
# Handle duplicate sections gracefully
|
||||
self.logger.error(f"enblocal.ini has duplicate [GLOBAL] sections: {e}")
|
||||
return False
|
||||
except configparser.Error as e:
|
||||
# Handle other configparser errors
|
||||
self.logger.error(f"ConfigParser error reading enblocal.ini: {e}")
|
||||
return False
|
||||
except Exception as e:
|
||||
# Handle any other errors
|
||||
self.logger.error(f"Unexpected error configuring enblocal.ini: {e}", exc_info=True)
|
||||
return False
|
||||
|
||||
def configure_enb_for_linux(self, modlist_path: Path) -> Tuple[bool, Optional[str], bool]:
|
||||
"""
|
||||
Main entry point: detect ENB and configure enblocal.ini.
|
||||
|
||||
Safe for modlists without ENB - returns success with no message.
|
||||
|
||||
Args:
|
||||
modlist_path: Path to modlist installation directory
|
||||
|
||||
Returns:
|
||||
Tuple[bool, Optional[str], bool]: (success, message, enb_detected)
|
||||
- success: True if successful or no ENB detected, False on error
|
||||
- message: Human-readable message (None if no action taken)
|
||||
- enb_detected: True if ENB was detected, False otherwise
|
||||
"""
|
||||
try:
|
||||
# Step 1: Detect ENB (safe - just searches for files)
|
||||
enb_info = self.detect_enb_in_modlist(modlist_path)
|
||||
enb_detected = enb_info.get('has_enb', False)
|
||||
|
||||
# Step 2: If no ENB detected, return success (no action needed)
|
||||
if not enb_detected:
|
||||
return (True, None, False) # Safe: no ENB, nothing to do
|
||||
|
||||
# Step 3: Find enblocal.ini
|
||||
enblocal_path = enb_info.get('enblocal_ini')
|
||||
if not enblocal_path:
|
||||
# ENB detected but no enblocal.ini found - this is unusual but not an error
|
||||
self.logger.warning("ENB detected but enblocal.ini not found - may be configured elsewhere")
|
||||
return (True, None, True) # ENB detected but no config file
|
||||
|
||||
# Step 4: Configure enblocal.ini (safe method with all checks)
|
||||
enblocal_path_obj = Path(enblocal_path)
|
||||
success = self.ensure_linux_version_setting(enblocal_path_obj)
|
||||
|
||||
if success:
|
||||
return (True, "ENB configured for Linux compatibility", True)
|
||||
else:
|
||||
# Non-blocking: log error but don't fail workflow
|
||||
return (False, "Failed to configure ENB (see logs for details)", True)
|
||||
|
||||
except Exception as e:
|
||||
# Catch-all error handling - never break the workflow
|
||||
self.logger.error(f"Error in ENB configuration: {e}", exc_info=True)
|
||||
return (False, "ENB configuration error (see logs)", False)
|
||||
|
||||
Reference in New Issue
Block a user