Last active
August 31, 2025 01:10
-
-
Save pythoninthegrass/ed0381708cea219784e84c2efc8a5f34 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env -S uv run --script | |
# /// script | |
# requires-python = ">=3.12" | |
# dependencies = [ | |
# "python-decouple>=3.8", | |
# ] | |
# [tool.uv] | |
# exclude-newer = "2025-07-21T00:00:00Z" | |
# /// | |
# pyright: reportMissingImports=false | |
""" | |
Clair Obscur: Expedition 33 Engine.ini Configuration Tool | |
A Python script to create and manage Engine.ini configuration files for | |
Clair Obscur: Expedition 33 with performance optimization settings. | |
Based on common UE5 SystemSettings tweaks for improved performance and visual quality. | |
""" | |
import argparse | |
import configparser | |
import shutil | |
import sys | |
from pathlib import Path | |
from typing import Dict, Any, Optional | |
class ClairObscurConfig: | |
"""Manage Engine.ini configurations for Clair Obscur: Expedition 33.""" | |
def __init__(self, config_path: Optional[Path] = None): | |
"""Initialize with Steam's default config path or custom path.""" | |
if config_path: | |
self.config_path = Path(config_path) | |
else: | |
# Default Steam Proton path | |
home = Path.home() | |
self.config_path = ( | |
home / ".var/app/com.valvesoftware.Steam/.local/share/Steam/" | |
"steamapps/compatdata/1903340/pfx/drive_c/users/steamuser/" | |
"Local Settings/Application Data/Sandfall/Saved/Config/Windows" | |
) | |
self.engine_ini_path = self.config_path / "Engine.ini" | |
# Ensure config directory exists | |
self.config_path.mkdir(parents=True, exist_ok=True) | |
def get_performance_preset(self, preset: str = "balanced") -> Dict[str, Dict[str, Any]]: | |
"""Get predefined performance presets.""" | |
presets = { | |
"low": { | |
"SystemSettings": { | |
# Basic performance settings | |
"r.ViewDistanceScale": 0.5, | |
"r.ShadowQuality": 0, | |
"r.PostProcessAAQuality": 0, | |
"r.MotionBlurQuality": 0, | |
"r.AmbientOcclusionLevels": 0, | |
"r.SSR.Quality": 0, | |
"r.RefractionQuality": 0, | |
"r.MaxAnisotropy": 4, | |
"r.LightFunctionQuality": 0, | |
"r.ShadowFilterQualityBias": -2, | |
"r.BloomQuality": 0, | |
"r.FastBlurThreshold": 0, | |
"r.Upscale.Quality": 1, | |
"r.DefaultFeature.AntiAliasing": 0, | |
# Lumen settings (low) | |
"r.Lumen.GlobalIllumination": "False", | |
"r.Lumen.Reflections": "False", | |
"r.Lumen.ScreenProbeGather": "False", | |
# Ray tracing off | |
"r.RayTracing": "False", | |
"r.RayTracing.Shadows": "False", | |
"r.RayTracing.Reflections": "False", | |
"r.RayTracing.GlobalIllumination": "False", | |
} | |
}, | |
"balanced": { | |
"SystemSettings": { | |
# Balanced settings for good performance/quality | |
"r.ViewDistanceScale": 0.85, | |
"r.ShadowQuality": 2, | |
"r.PostProcessAAQuality": 2, | |
"r.MotionBlurQuality": 1, | |
"r.AmbientOcclusionLevels": 2, | |
"r.SSR.Quality": 2, | |
"r.RefractionQuality": 1, | |
"r.MaxAnisotropy": 8, | |
"r.LightFunctionQuality": 1, | |
"r.ShadowFilterQualityBias": 0, | |
"r.BloomQuality": 2, | |
"r.FastBlurThreshold": 2, | |
"r.Upscale.Quality": 2, | |
"r.DefaultFeature.AntiAliasing": 2, | |
# Lumen settings (balanced) | |
"r.Lumen.GlobalIllumination": "True", | |
"r.Lumen.Reflections": "True", | |
"r.Lumen.ScreenProbeGather": "True", | |
"r.Lumen.Reflections.HardwareRayTracing": "False", | |
# Conservative ray tracing | |
"r.RayTracing": "False", | |
"r.RayTracing.Shadows": "False", | |
"r.RayTracing.Reflections": "False", | |
"r.RayTracing.GlobalIllumination": "False", | |
} | |
}, | |
"ultra": { | |
"SystemSettings": { | |
# High-end settings for maximum quality | |
"r.ViewDistanceScale": 1.5, | |
"r.ShadowQuality": 4, | |
"r.PostProcessAAQuality": 4, | |
"r.MotionBlurQuality": 4, | |
"r.AmbientOcclusionLevels": 4, | |
"r.SSR.Quality": 4, | |
"r.RefractionQuality": 4, | |
"r.MaxAnisotropy": 16, | |
"r.LightFunctionQuality": 4, | |
"r.ShadowFilterQualityBias": 2, | |
"r.BloomQuality": 4, | |
"r.FastBlurThreshold": 100, | |
"r.Upscale.Quality": 4, | |
"r.DefaultFeature.AntiAliasing": 4, | |
# Lumen settings (ultra) | |
"r.Lumen.GlobalIllumination": "True", | |
"r.Lumen.Reflections": "True", | |
"r.Lumen.ScreenProbeGather": "True", | |
"r.Lumen.Reflections.HardwareRayTracing": "True", | |
# Ray tracing enabled | |
"r.RayTracing": "True", | |
"r.RayTracing.Shadows": "True", | |
"r.RayTracing.Reflections": "True", | |
"r.RayTracing.GlobalIllumination": "True", | |
"r.RayTracing.AmbientOcclusion": "True", | |
} | |
}, | |
"sharp_clear": { | |
"SystemSettings": { | |
# Sharp and clear preset (disable blur/grain effects) | |
"r.Tonemapper.GrainQuantization": 0, | |
"r.Tonemapper.Quality": 0, | |
"r.PostProcessAAQuality": 0, | |
"r.AntiAliasingMethod": 0, | |
"r.DefaultFeature.AntiAliasing": 0, | |
"r.MotionBlurQuality": 0, | |
"r.DepthOfFieldQuality": 0, | |
"r.BloomQuality": 0, | |
"r.LensFlareQuality": 0, | |
"r.SceneColorFringeQuality": 0, | |
"r.FilmGrain": 0, | |
"r.Tonemapper.Sharpen": 1.5, | |
"r.Upscale.Softness": 0, | |
} | |
} | |
} | |
return presets.get(preset, presets["balanced"]) | |
def get_engine_tweaks(self) -> Dict[str, Dict[str, Any]]: | |
"""Additional engine tweaks for better performance.""" | |
return { | |
"/Script/Engine.Engine": { | |
"bSmoothFrameRate": "False", | |
"bUseFixedFrameRate": "False", | |
"SmoothedFrameRateRange": "(LowerBound=(Type=Inclusive,Value=30.000000),UpperBound=(Type=Exclusive,Value=120.000000))", | |
"MinDesiredFrameRate": 30, | |
}, | |
"/Script/Engine.RendererSettings": { | |
"r.AntiAliasingMethod": 0, | |
"r.MSAACount": 1, | |
"r.DefaultFeature.AntiAliasing": 0, | |
"r.DefaultFeature.AutoExposure.ExtendDefaultLuminanceRange": "True", | |
}, | |
"/Script/Engine.NetworkSettings": { | |
"n.VerifyPeer": "False", | |
} | |
} | |
def backup_existing_config(self) -> Optional[Path]: | |
"""Create a backup of existing Engine.ini if it exists.""" | |
if self.engine_ini_path.exists(): | |
backup_path = self.engine_ini_path.with_suffix(".ini.backup") | |
shutil.copy2(self.engine_ini_path, backup_path) | |
print(f"Backup created: {backup_path}") | |
return backup_path | |
return None | |
def create_engine_ini(self, preset: str = "balanced", include_tweaks: bool = True) -> None: | |
"""Create Engine.ini with specified preset and tweaks.""" | |
config = configparser.ConfigParser() | |
config.optionxform = str # Preserve case sensitivity | |
# Add preset settings | |
preset_config = self.get_performance_preset(preset) | |
for section, settings in preset_config.items(): | |
config[section] = {} | |
for key, value in settings.items(): | |
config[section][key] = str(value) | |
# Add engine tweaks if requested | |
if include_tweaks: | |
tweak_config = self.get_engine_tweaks() | |
for section, settings in tweak_config.items(): | |
if section not in config: | |
config[section] = {} | |
for key, value in settings.items(): | |
config[section][key] = str(value) | |
# Write config file | |
with open(self.engine_ini_path, 'w') as f: | |
config.write(f) | |
print(f"Engine.ini created: {self.engine_ini_path}") | |
print(f"Applied preset: {preset}") | |
def apply_custom_settings(self, custom_settings: Dict[str, Dict[str, Any]]) -> None: | |
"""Apply custom settings to existing Engine.ini.""" | |
config = configparser.ConfigParser() | |
config.optionxform = str | |
# Read existing config if it exists | |
if self.engine_ini_path.exists(): | |
config.read(self.engine_ini_path) | |
# Apply custom settings | |
for section, settings in custom_settings.items(): | |
if section not in config: | |
config[section] = {} | |
for key, value in settings.items(): | |
config[section][key] = str(value) | |
# Write updated config | |
with open(self.engine_ini_path, 'w') as f: | |
config.write(f) | |
print(f"Custom settings applied to: {self.engine_ini_path}") | |
def set_read_only(self, read_only: bool = True) -> None: | |
"""Set Engine.ini as read-only to prevent game from overwriting.""" | |
if self.engine_ini_path.exists(): | |
current_stat = self.engine_ini_path.stat() | |
if read_only: | |
# Remove write permissions | |
self.engine_ini_path.chmod(current_stat.st_mode & ~0o200) | |
print("Engine.ini set to read-only") | |
else: | |
# Add write permissions | |
self.engine_ini_path.chmod(current_stat.st_mode | 0o200) | |
print("Engine.ini write permissions restored") | |
def show_current_config(self) -> None: | |
"""Display current Engine.ini configuration.""" | |
if not self.engine_ini_path.exists(): | |
print("Engine.ini does not exist") | |
return | |
config = configparser.ConfigParser() | |
config.optionxform = str | |
config.read(self.engine_ini_path) | |
print(f"Current Engine.ini configuration ({self.engine_ini_path}):") | |
print("=" * 60) | |
for section in config.sections(): | |
print(f"\n[{section}]") | |
for key, value in config[section].items(): | |
print(f"{key}={value}") | |
def main(): | |
"""Main CLI interface.""" | |
parser = argparse.ArgumentParser( | |
description="Clair Obscur: Expedition 33 Engine.ini Configuration Tool" | |
) | |
parser.add_argument( | |
"--config-path", | |
type=Path, | |
help="Custom path to config directory (default: Steam Proton path)" | |
) | |
subparsers = parser.add_subparsers(dest="command", help="Available commands") | |
# Create command | |
create_parser = subparsers.add_parser("create", help="Create new Engine.ini") | |
create_parser.add_argument( | |
"--preset", | |
choices=["low", "balanced", "ultra", "sharp_clear"], | |
default="balanced", | |
help="Performance preset to apply" | |
) | |
create_parser.add_argument( | |
"--no-tweaks", | |
action="store_true", | |
help="Don't include additional engine tweaks" | |
) | |
create_parser.add_argument( | |
"--read-only", | |
action="store_true", | |
help="Set Engine.ini as read-only after creation" | |
) | |
# Show command | |
subparsers.add_parser("show", help="Show current Engine.ini configuration") | |
# Backup command | |
subparsers.add_parser("backup", help="Create backup of current Engine.ini") | |
# Custom command | |
custom_parser = subparsers.add_parser("custom", help="Apply custom settings") | |
custom_parser.add_argument( | |
"--section", | |
required=True, | |
help="Config section name" | |
) | |
custom_parser.add_argument( | |
"--setting", | |
action="append", | |
nargs=2, | |
metavar=("KEY", "VALUE"), | |
help="Custom setting key-value pair (can be used multiple times)" | |
) | |
# Read-only command | |
readonly_parser = subparsers.add_parser("readonly", help="Set read-only status") | |
readonly_parser.add_argument( | |
"status", | |
choices=["on", "off"], | |
help="Enable or disable read-only mode" | |
) | |
args = parser.parse_args() | |
if not args.command: | |
parser.print_help() | |
return | |
# Initialize config manager | |
config_manager = ClairObscurConfig(args.config_path) | |
try: | |
if args.command == "create": | |
config_manager.backup_existing_config() | |
config_manager.create_engine_ini( | |
preset=args.preset, | |
include_tweaks=not args.no_tweaks | |
) | |
if args.read_only: | |
config_manager.set_read_only(True) | |
elif args.command == "show": | |
config_manager.show_current_config() | |
elif args.command == "backup": | |
backup_path = config_manager.backup_existing_config() | |
if not backup_path: | |
print("No Engine.ini found to backup") | |
elif args.command == "custom": | |
if not args.setting: | |
print("Error: --setting is required for custom command") | |
return | |
custom_settings = {args.section: dict(args.setting)} | |
config_manager.apply_custom_settings(custom_settings) | |
elif args.command == "readonly": | |
config_manager.set_read_only(args.status == "on") | |
except Exception as e: | |
print(f"Error: {e}", file=sys.stderr) | |
sys.exit(1) | |
if __name__ == "__main__": | |
main() |
Author
pythoninthegrass
commented
Aug 31, 2025
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment