mirror of
https://github.com/Omni-guides/Jackify.git
synced 2026-01-17 11:37:01 +01:00
318 lines
13 KiB
Python
318 lines
13 KiB
Python
#!/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)
|
|
|