From 62df0441f1ddc83c31dccc7cf9c62a762aed4994 Mon Sep 17 00:00:00 2001 From: wuriko Date: Sun, 8 Feb 2026 15:08:04 +0100 Subject: feat: add support for optiscaler config variables --- defaults/assets/fgmod.sh | 5 +++ defaults/assets/update-optiscaler-config.py | 66 +++++++++++++++++++++++++++++ main.py | 9 ++++ 3 files changed, 80 insertions(+) create mode 100644 defaults/assets/update-optiscaler-config.py diff --git a/defaults/assets/fgmod.sh b/defaults/assets/fgmod.sh index d48856d..efe1ef2 100755 --- a/defaults/assets/fgmod.sh +++ b/defaults/assets/fgmod.sh @@ -128,6 +128,11 @@ else logger -t fgmod "📄 OptiScaler.ini installed to $exe_folder_path" fi +# === OptiScaler env variables Handling === +if command -v python &> /dev/null && [ -f "$fgmod_path/update-optiscaler-config.py" ]; then + python "$fgmod_path/update-optiscaler-config.py" "$exe_folder_path/OptiScaler.ini" +fi + # === ASI Plugins Directory === if [[ -d "$fgmod_path/plugins" ]]; then echo "🔌 Installing ASI plugins directory" diff --git a/defaults/assets/update-optiscaler-config.py b/defaults/assets/update-optiscaler-config.py new file mode 100644 index 0000000..9c00ee1 --- /dev/null +++ b/defaults/assets/update-optiscaler-config.py @@ -0,0 +1,66 @@ +import os +import sys +import re + +def update_optiscaler_config(file_path): + if not os.path.exists(file_path): + print(f"Error: File '{file_path}' not found.") + return + + with open(file_path, 'r') as f: + lines = f.readlines() + + # Get environment variables starting with OptiScaler_ + env_vars = {k: v for k, v in os.environ.items() if k.startswith("OptiScaler_")} + + for env_name, env_value in env_vars.items(): + # Split OptiScaler_Section_Key + parts = env_name.split('_', 2) + if len(parts) < 3: + continue + + section_target = parts[1] + key_target = parts[2] + + found_section = False + updated_key = False + + # Regex to match [Section] and Key=Value + section_pattern = re.compile(rf'^\s*\[{re.escape(section_target)}\]\s*') + key_pattern = re.compile(rf'^(\s*{re.escape(key_target)}\s*)=.*') + + for i, line in enumerate(lines): + # Track if we are inside the correct section + if section_pattern.match(line): + found_section = True + continue + + # If we hit a new section before finding the key, the key doesn't exist in the target section + if found_section and line.strip().startswith('[') and not section_pattern.match(line): + break + + # Replace the value if the key is found within the correct section + if found_section and key_pattern.match(line): + lines[i] = key_pattern.sub(r'\1=' + env_value, line) + updated_key = True + print(f"Updated: [{section_target}] {key_target} = {env_value}") + break + + # If section exists but key doesn't, append key to the end of the section + # Not really needed but supported just in case + if found_section and not updated_key: + for i, line in enumerate(lines): + if section_pattern.match(line): + lines.insert(i + 1, f"{key_target}={env_value}\n") + print(f"Added new key: [{section_target}] {key_target} = {env_value}") + break + + # Write the modified content back + with open(file_path, 'w') as f: + f.writelines(lines) + +if __name__ == "__main__": + if len(sys.argv) < 2: + print("Usage: python update-optiscaler-config.py ") + else: + update_optiscaler_config(sys.argv[1]) \ No newline at end of file diff --git a/main.py b/main.py index 5b6b8e8..5a9c874 100644 --- a/main.py +++ b/main.py @@ -114,6 +114,14 @@ class Plugin: shutil.copy2(uninstaller_src, uninstaller_dest) uninstaller_dest.chmod(0o755) decky.logger.info(f"Copied uninstaller script to {uninstaller_dest}") + + # Copy optiscaler config updater script + optiscaler_config_updater_src = assets_dir / "update-optiscaler-config.py" + optiscaler_config_updater_dest = extract_path / "update-optiscaler-config.py" + if optiscaler_config_updater_src.exists(): + shutil.copy2(optiscaler_config_updater_src, optiscaler_config_updater_dest) + optiscaler_config_updater_dest.chmod(0o755) + decky.logger.info(f"Copied update-optiscaler-config.py script to {optiscaler_config_updater_dest}") return True except Exception as e: @@ -413,6 +421,7 @@ class Plugin: "libxell.dll", # New in v0.9.0-pre4 "fgmod", "fgmod-uninstaller.sh" + "update-optiscaler-config.py" ] if path.exists(): -- cgit v1.2.3 From c17dba2d1ceebf2fae54e5ded0f3481ee2e834cd Mon Sep 17 00:00:00 2001 From: wuriko Date: Sun, 8 Feb 2026 15:43:40 +0100 Subject: fix: fix check_fgmod_path --- main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.py b/main.py index 5a9c874..1e45149 100644 --- a/main.py +++ b/main.py @@ -420,7 +420,7 @@ class Plugin: "libxess_fg.dll", # New in v0.9.0-pre4 "libxell.dll", # New in v0.9.0-pre4 "fgmod", - "fgmod-uninstaller.sh" + "fgmod-uninstaller.sh", "update-optiscaler-config.py" ] -- cgit v1.2.3 From 173a50320c5085b1b55cf43380f4cd5650a71b67 Mon Sep 17 00:00:00 2001 From: wuriko Date: Tue, 10 Feb 2026 20:07:53 +0100 Subject: refactor: remove unnecessary add key step --- defaults/assets/update-optiscaler-config.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/defaults/assets/update-optiscaler-config.py b/defaults/assets/update-optiscaler-config.py index 9c00ee1..1fce84a 100644 --- a/defaults/assets/update-optiscaler-config.py +++ b/defaults/assets/update-optiscaler-config.py @@ -23,7 +23,6 @@ def update_optiscaler_config(file_path): key_target = parts[2] found_section = False - updated_key = False # Regex to match [Section] and Key=Value section_pattern = re.compile(rf'^\s*\[{re.escape(section_target)}\]\s*') @@ -42,19 +41,9 @@ def update_optiscaler_config(file_path): # Replace the value if the key is found within the correct section if found_section and key_pattern.match(line): lines[i] = key_pattern.sub(r'\1=' + env_value, line) - updated_key = True print(f"Updated: [{section_target}] {key_target} = {env_value}") break - # If section exists but key doesn't, append key to the end of the section - # Not really needed but supported just in case - if found_section and not updated_key: - for i, line in enumerate(lines): - if section_pattern.match(line): - lines.insert(i + 1, f"{key_target}={env_value}\n") - print(f"Added new key: [{section_target}] {key_target} = {env_value}") - break - # Write the modified content back with open(file_path, 'w') as f: f.writelines(lines) -- cgit v1.2.3 From 5758dea85bf35a6b7efa8883de059dd5184ac417 Mon Sep 17 00:00:00 2001 From: wuriko Date: Tue, 10 Feb 2026 21:22:27 +0100 Subject: feat: handle unprefixed env variables --- defaults/assets/update-optiscaler-config.py | 58 ++++++++++++++++++++++++----- 1 file changed, 49 insertions(+), 9 deletions(-) diff --git a/defaults/assets/update-optiscaler-config.py b/defaults/assets/update-optiscaler-config.py index 1fce84a..bd040b3 100644 --- a/defaults/assets/update-optiscaler-config.py +++ b/defaults/assets/update-optiscaler-config.py @@ -1,6 +1,7 @@ import os import sys import re +from configparser import ConfigParser def update_optiscaler_config(file_path): if not os.path.exists(file_path): @@ -10,18 +11,57 @@ def update_optiscaler_config(file_path): with open(file_path, 'r') as f: lines = f.readlines() - # Get environment variables starting with OptiScaler_ - env_vars = {k: v for k, v in os.environ.items() if k.startswith("OptiScaler_")} + config = ConfigParser() + config.read(file_path) - for env_name, env_value in env_vars.items(): - # Split OptiScaler_Section_Key + # Because we want to support unprefixed env variables, we need to count key occurrences across all sections of the ini file + # Keys that appear multiple times should be prefixed like Section_Key by the user for them to be targeted properly + + # Note: keys returned by configparser are in lowercase + key_occurrences = {} + key_to_sections = {} + for section in config.sections(): + for key in config.options(section): + key_occurrences[key] = key_occurrences.get(key, 0) + 1 + if key not in key_to_sections: + key_to_sections[key] = [] + key_to_sections[key].append(section) + + print(key_occurrences) + print(key_to_sections) + + env_updates = [] + + # Handle OptiScaler_Section_Key format + optiscaler_vars = {k: v for k, v in os.environ.items() if k.startswith("OptiScaler_")} + for env_name, env_value in optiscaler_vars.items(): parts = env_name.split('_', 2) - if len(parts) < 3: - continue + if len(parts) >= 3: + env_updates.append(('optiscaler', parts[1], parts[2], env_value, env_name)) + + # Handle Section_Key and Key formats + other_vars = {k: v for k, v in os.environ.items() if not k.startswith("OptiScaler_")} + for env_name, env_value in other_vars.items(): + # Try Section_Key format + if '_' in env_name: + parts = env_name.split('_', 1) + section = parts[0] + key = parts[1] + # Check if this section exists and has this key + if config.has_section(section) and config.has_option(section, key): + env_updates.append(('section_key', section, key, env_value, env_name)) + continue + + # Try Key format + if env_name.lower() in key_occurrences and key_occurrences[env_name.lower()] == 1: + section = key_to_sections[env_name.lower()][0] + env_updates.append(('key', section, env_name, env_value, env_name)) - section_target = parts[1] - key_target = parts[2] + print(f"Found {len(env_updates)} updates to apply") + for entry in env_updates: + print(f"> {entry}") + for update_type, section_target, key_target, env_value, env_name in env_updates: found_section = False # Regex to match [Section] and Key=Value @@ -41,7 +81,7 @@ def update_optiscaler_config(file_path): # Replace the value if the key is found within the correct section if found_section and key_pattern.match(line): lines[i] = key_pattern.sub(r'\1=' + env_value, line) - print(f"Updated: [{section_target}] {key_target} = {env_value}") + print(f"Updated: [{section_target}] {key_target} = {env_value} (from {env_name})") break # Write the modified content back -- cgit v1.2.3 From 5d70a558ad89a6803ea5c579fd412c3aae559dc9 Mon Sep 17 00:00:00 2001 From: wuriko Date: Tue, 10 Feb 2026 21:33:09 +0100 Subject: fix: use case sensitive values only to prevent PATH to accidentally match --- defaults/assets/update-optiscaler-config.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/defaults/assets/update-optiscaler-config.py b/defaults/assets/update-optiscaler-config.py index bd040b3..7615b48 100644 --- a/defaults/assets/update-optiscaler-config.py +++ b/defaults/assets/update-optiscaler-config.py @@ -12,12 +12,12 @@ def update_optiscaler_config(file_path): lines = f.readlines() config = ConfigParser() + config.optionxform = str # Preserve case for keys (otherwise PATH could match Path) config.read(file_path) # Because we want to support unprefixed env variables, we need to count key occurrences across all sections of the ini file # Keys that appear multiple times should be prefixed like Section_Key by the user for them to be targeted properly - # Note: keys returned by configparser are in lowercase key_occurrences = {} key_to_sections = {} for section in config.sections(): @@ -27,9 +27,6 @@ def update_optiscaler_config(file_path): key_to_sections[key] = [] key_to_sections[key].append(section) - print(key_occurrences) - print(key_to_sections) - env_updates = [] # Handle OptiScaler_Section_Key format @@ -52,9 +49,9 @@ def update_optiscaler_config(file_path): env_updates.append(('section_key', section, key, env_value, env_name)) continue - # Try Key format - if env_name.lower() in key_occurrences and key_occurrences[env_name.lower()] == 1: - section = key_to_sections[env_name.lower()][0] + # Try Key format (only if key appears exactly once across all sections) + if env_name in key_occurrences and key_occurrences[env_name] == 1: + section = key_to_sections[env_name][0] env_updates.append(('key', section, env_name, env_value, env_name)) print(f"Found {len(env_updates)} updates to apply") @@ -64,8 +61,8 @@ def update_optiscaler_config(file_path): for update_type, section_target, key_target, env_value, env_name in env_updates: found_section = False - # Regex to match [Section] and Key=Value - section_pattern = re.compile(rf'^\s*\[{re.escape(section_target)}\]\s*') + # Regex to match [Section] and Key=Value (case-sensitive) + section_pattern = re.compile(rf'^\s*\[{re.escape(section_target)}]\s*') key_pattern = re.compile(rf'^(\s*{re.escape(key_target)}\s*)=.*') for i, line in enumerate(lines): -- cgit v1.2.3 From 128ae72f211f35d7a9d238349b586bc994536a71 Mon Sep 17 00:00:00 2001 From: wuriko Date: Wed, 11 Feb 2026 07:45:43 +0100 Subject: feat: add hyphenated section name support (V-Sync) --- defaults/assets/update-optiscaler-config.py | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/defaults/assets/update-optiscaler-config.py b/defaults/assets/update-optiscaler-config.py index 7615b48..f4a65de 100644 --- a/defaults/assets/update-optiscaler-config.py +++ b/defaults/assets/update-optiscaler-config.py @@ -18,9 +18,19 @@ def update_optiscaler_config(file_path): # Because we want to support unprefixed env variables, we need to count key occurrences across all sections of the ini file # Keys that appear multiple times should be prefixed like Section_Key by the user for them to be targeted properly + # Normalize section names: strip - and . so V-Sync becomes VSync + # This allows env vars like VSync_Key to match INI section [V-Sync] + def normalize_section(section_name): + return section_name.replace('-', '').replace('.', '') + key_occurrences = {} key_to_sections = {} + section_normalized_to_actual = {} # Maps normalized section name to actual section name + for section in config.sections(): + normalized = normalize_section(section) + section_normalized_to_actual[normalized] = section + for key in config.options(section): key_occurrences[key] = key_occurrences.get(key, 0) + 1 if key not in key_to_sections: @@ -42,13 +52,21 @@ def update_optiscaler_config(file_path): # Try Section_Key format if '_' in env_name: parts = env_name.split('_', 1) - section = parts[0] + section_from_env = parts[0] key = parts[1] - # Check if this section exists and has this key - if config.has_section(section) and config.has_option(section, key): - env_updates.append(('section_key', section, key, env_value, env_name)) + + # Try exact section match first + if config.has_section(section_from_env) and config.has_option(section_from_env, key): + env_updates.append(('section_key', section_from_env, key, env_value, env_name)) continue + # Try section match with normalized section names + if section_from_env in section_normalized_to_actual: + actual_section = section_normalized_to_actual[section_from_env] + if config.has_option(actual_section, key): + env_updates.append(('section_key', actual_section, key, env_value, env_name)) + continue + # Try Key format (only if key appears exactly once across all sections) if env_name in key_occurrences and key_occurrences[env_name] == 1: section = key_to_sections[env_name][0] -- cgit v1.2.3 From 938110d2d94f3a51887fb63ed4d74b9a29728e55 Mon Sep 17 00:00:00 2001 From: wuriko Date: Wed, 11 Feb 2026 07:46:54 +0100 Subject: refactor: remove unnecessary python check --- defaults/assets/fgmod.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/defaults/assets/fgmod.sh b/defaults/assets/fgmod.sh index efe1ef2..30d74ac 100755 --- a/defaults/assets/fgmod.sh +++ b/defaults/assets/fgmod.sh @@ -129,7 +129,7 @@ else fi # === OptiScaler env variables Handling === -if command -v python &> /dev/null && [ -f "$fgmod_path/update-optiscaler-config.py" ]; then +if [[ -f "$fgmod_path/update-optiscaler-config.py" ]]; then python "$fgmod_path/update-optiscaler-config.py" "$exe_folder_path/OptiScaler.ini" fi -- cgit v1.2.3 From 6b9d38927f21b83e84af90205828be0120a5d52a Mon Sep 17 00:00:00 2001 From: wuriko Date: Thu, 12 Feb 2026 22:39:09 +0100 Subject: docs: update README with env var support --- README.md | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/README.md b/README.md index 70b8131..d188cd3 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,46 @@ This plugin uses OptiScaler to replace DLSS calls with FSR3/FSR3.1, giving you: - Click "Copy Unpatch Command" and replace the launch options with: `~/fgmod/fgmod-uninstaller.sh %command%` - Run the game at least once to make the uninstaller script run. After you can leave the launch option or remove it +### Configuring OptiScaler via Environment Variables +Starting v0.14.0, you can update OptiScaler settings before a game launches by adding environment variables. +This is useful if you plan to use the same settings across multiple games so they are pre-configured by the time you launch them. + +For example, considering the following sample from the OptiScaler.ini config file: +``` +[Upscalers] +Dx11Upscaler=auto +Dx12Upscaler=auto +VulkanUpscaler=auto + +[FrameGen] +Enabled=auto +FGInput=auto +FGOutput=auto +DebugView=auto +DrawUIOverFG=auto +``` +We can decide to set `Dx12Upscaler=fsr31` to enable FSR4 in DX12 games by default. This works because the option name `Dx12Upscaler` is unique throughout the file but for options that appear multiple times like `Enabled`, you can prefix the option name with the section name like `FrameGen_Enabled=true`. +You can provide section names for all options if you want to be explicit. You can also prefix `Section_Option` with `OptiScaler` to ensure no conflict with other commands. + +Here's the breakdown of supported formats: +- `OptiScaler_Section_Option=value` - Full format (foolproof) +- `Section_Option=value` - Short format (recommended) +- `Option=value` - Minimal format (only works if the option name appears once in OptiScaler.ini) + +**Example:** +```bash +# Enable frame generation with XeFG output +FrameGen_Enabled=true FGInput=fsrfg FGOutput=xefg ~/fgmod/fgmod %command% + +# Set DX12 upscaler to FSR 3.1 (Upgrades to FSR4) +Dx12Upscaler=fsr31 ~/fgmod/fgmod %command% +``` + +**Notes:** +- Environment variables override the OptiScaler.ini file on each game launch +- Hyphenated section names like `[V-Sync]` can be accessed like `VSync_Option=value` +- If an option name appears in multiple sections of the OptiScaler.ini file, use the `Section_Option` or `OptiScaler_Section_Option` format + ## Technical Details ### What's Included -- cgit v1.2.3