From c02343e68874efd57c2e312cb6b7e4f02222e43a Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Tue, 22 Jul 2025 11:11:44 -0400 Subject: add workaround env vars, rm old tests --- package.json | 2 +- py_modules/lsfg_vk/config_schema.py | 28 ++++- py_modules/lsfg_vk/configuration.py | 19 +++- py_modules/lsfg_vk/plugin.py | 9 +- shared_config.py | 14 +++ src/api/lsfgApi.ts | 4 +- src/components/ConfigurationSection.tsx | 18 +++ src/config/configSchema.ts | 6 +- src/config/generatedConfigSchema.ts | 18 +++ tests/conftest.py | 31 ------ tests/test_configuration.py | 190 -------------------------------- tests/test_dll_detection.py | 129 ---------------------- tests/test_installation.py | 150 ------------------------- 13 files changed, 106 insertions(+), 512 deletions(-) delete mode 100644 tests/conftest.py delete mode 100644 tests/test_configuration.py delete mode 100644 tests/test_dll_detection.py delete mode 100644 tests/test_installation.py diff --git a/package.json b/package.json index 9fcf10d..ee18530 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "decky-lossless-scaling-vk", - "version": "0.6.9", + "version": "0.6.10", "description": "Use Lossless Scaling on the Steam Deck using the lsfg-vk vulkan layer", "type": "module", "scripts": { diff --git a/py_modules/lsfg_vk/config_schema.py b/py_modules/lsfg_vk/config_schema.py index c82d1d3..6a68db1 100644 --- a/py_modules/lsfg_vk/config_schema.py +++ b/py_modules/lsfg_vk/config_schema.py @@ -73,6 +73,20 @@ SCRIPT_ONLY_FIELDS = { field_type=ConfigFieldType.BOOLEAN, default=False, description="disable Steam Deck mode (unlocks hidden settings in some games)" + ), + + "mangohud_workaround": ConfigField( + name="mangohud_workaround", + field_type=ConfigFieldType.BOOLEAN, + default=False, + description="Enables a transparent mangohud overlay, sometimes fixes issues with 2X multiplier in game mode" + ), + + "disable_vkbasalt": ConfigField( + name="disable_vkbasalt", + field_type=ConfigFieldType.BOOLEAN, + default=False, + description="Disables vkBasalt layer which can conflict with LSFG (Reshade, some Decky plugins)" ) } @@ -91,6 +105,8 @@ class ConfigurationData(TypedDict): dxvk_frame_rate: int enable_wow64: bool disable_steamdeck_mode: bool + mangohud_workaround: bool + disable_vkbasalt: bool class ConfigurationManager: @@ -335,6 +351,10 @@ class ConfigurationManager: script_values["enable_wow64"] = value == "1" elif key == "SteamDeck": script_values["disable_steamdeck_mode"] = value == "0" + elif key == "MANGOHUD": + script_values["mangohud_workaround"] = value == "1" + elif key == "DISABLE_VKBASALT": + script_values["disable_vkbasalt"] = value == "1" except (ValueError, KeyError, IndexError) as e: # If parsing fails, log the error and return empty dict (will use defaults) @@ -368,7 +388,9 @@ class ConfigurationManager: experimental_present_mode: str = "fifo", dxvk_frame_rate: int = 0, enable_wow64: bool = False, - disable_steamdeck_mode: bool = False) -> ConfigurationData: + disable_steamdeck_mode: bool = False, + mangohud_workaround: bool = False, + disable_vkbasalt: bool = False) -> ConfigurationData: """Create configuration from individual arguments""" return cast(ConfigurationData, { "dll": dll, @@ -379,5 +401,7 @@ class ConfigurationManager: "experimental_present_mode": experimental_present_mode, "dxvk_frame_rate": dxvk_frame_rate, "enable_wow64": enable_wow64, - "disable_steamdeck_mode": disable_steamdeck_mode + "disable_steamdeck_mode": disable_steamdeck_mode, + "mangohud_workaround": mangohud_workaround, + "disable_vkbasalt": disable_vkbasalt }) diff --git a/py_modules/lsfg_vk/configuration.py b/py_modules/lsfg_vk/configuration.py index 47d0ebc..82982e5 100644 --- a/py_modules/lsfg_vk/configuration.py +++ b/py_modules/lsfg_vk/configuration.py @@ -65,7 +65,9 @@ class ConfigurationService(BaseService): experimental_present_mode: str = "fifo", dxvk_frame_rate: int = 0, enable_wow64: bool = False, - disable_steamdeck_mode: bool = False) -> ConfigurationResponse: + disable_steamdeck_mode: bool = False, + mangohud_workaround: bool = False, + disable_vkbasalt: bool = False) -> ConfigurationResponse: """Update TOML configuration Args: @@ -78,6 +80,8 @@ class ConfigurationService(BaseService): dxvk_frame_rate: Frame rate cap for DirectX games, before frame multiplier (0 = disabled) enable_wow64: Whether to enable PROTON_USE_WOW64=1 for 32-bit games disable_steamdeck_mode: Whether to disable Steam Deck mode + mangohud_workaround: Whether to enable MangoHud workaround with transparent overlay + disable_vkbasalt: Whether to disable vkBasalt layer Returns: ConfigurationResponse with success status @@ -86,7 +90,8 @@ class ConfigurationService(BaseService): # Create configuration from individual arguments config = ConfigurationManager.create_config_from_args( dll, multiplier, flow_scale, performance_mode, hdr_mode, - experimental_present_mode, dxvk_frame_rate, enable_wow64, disable_steamdeck_mode + experimental_present_mode, dxvk_frame_rate, enable_wow64, disable_steamdeck_mode, + mangohud_workaround, disable_vkbasalt ) # Generate TOML content using centralized manager @@ -108,7 +113,8 @@ class ConfigurationService(BaseService): f"performance_mode={performance_mode}, hdr_mode={hdr_mode}, " f"experimental_present_mode='{experimental_present_mode}', " f"dxvk_frame_rate={dxvk_frame_rate}, " - f"enable_wow64={enable_wow64}, disable_steamdeck_mode={disable_steamdeck_mode}") + f"enable_wow64={enable_wow64}, disable_steamdeck_mode={disable_steamdeck_mode}, " + f"mangohud_workaround={mangohud_workaround}, disable_vkbasalt={disable_vkbasalt}") return self._success_response(ConfigurationResponse, "lsfg configuration updated successfully", @@ -214,6 +220,13 @@ class ConfigurationService(BaseService): if config.get("disable_steamdeck_mode", False): lines.append("export SteamDeck=0") + if config.get("mangohud_workaround", False): + lines.append("export MANGOHUD=1") + lines.append("export MANGOHUD_CONFIG=alpha=0.01,background_alpha=0.01") + + if config.get("disable_vkbasalt", False): + lines.append("export DISABLE_VKBASALT=1") + # Add DXVK_FRAME_RATE if dxvk_frame_rate is set dxvk_frame_rate = config.get("dxvk_frame_rate", 0) if dxvk_frame_rate > 0: diff --git a/py_modules/lsfg_vk/plugin.py b/py_modules/lsfg_vk/plugin.py index 425d7e7..7deed71 100644 --- a/py_modules/lsfg_vk/plugin.py +++ b/py_modules/lsfg_vk/plugin.py @@ -189,7 +189,9 @@ class Plugin: experimental_present_mode: str = "fifo", dxvk_frame_rate: int = 0, enable_wow64: bool = False, - disable_steamdeck_mode: bool = False) -> Dict[str, Any]: + disable_steamdeck_mode: bool = False, + mangohud_workaround: bool = False, + disable_vkbasalt: bool = False) -> Dict[str, Any]: """Update lsfg TOML configuration Args: @@ -202,13 +204,16 @@ class Plugin: dxvk_frame_rate: Frame rate cap for DirectX games, before frame multiplier (0 = disabled) enable_wow64: Whether to enable PROTON_USE_WOW64=1 for 32-bit games disable_steamdeck_mode: Whether to disable Steam Deck mode + mangohud_workaround: Whether to enable MangoHud workaround with transparent overlay + disable_vkbasalt: Whether to disable vkBasalt layer Returns: ConfigurationResponse dict with success status """ return self.configuration_service.update_config( dll, multiplier, flow_scale, performance_mode, hdr_mode, - experimental_present_mode, dxvk_frame_rate, enable_wow64, disable_steamdeck_mode + experimental_present_mode, dxvk_frame_rate, enable_wow64, disable_steamdeck_mode, + mangohud_workaround, disable_vkbasalt ) async def update_dll_path(self, dll_path: str) -> Dict[str, Any]: diff --git a/shared_config.py b/shared_config.py index 46bfc84..518294d 100644 --- a/shared_config.py +++ b/shared_config.py @@ -81,6 +81,20 @@ CONFIG_SCHEMA_DEF = { "fieldType": ConfigFieldType.BOOLEAN, "default": False, "description": "disable Steam Deck mode (unlocks hidden settings in some games)" + }, + + "mangohud_workaround": { + "name": "mangohud_workaround", + "fieldType": ConfigFieldType.BOOLEAN, + "default": False, + "description": "Enables a transparent mangohud overlay, sometimes fixes issues with 2X multiplier in game mode" + }, + + "disable_vkbasalt": { + "name": "disable_vkbasalt", + "fieldType": ConfigFieldType.BOOLEAN, + "default": False, + "description": "Disables vkBasalt layer which can conflict with LSFG (Reshade, some Decky plugins)" } } diff --git a/src/api/lsfgApi.ts b/src/api/lsfgApi.ts index 74caa57..0b221e9 100644 --- a/src/api/lsfgApi.ts +++ b/src/api/lsfgApi.ts @@ -101,14 +101,14 @@ export const getLaunchScriptContent = callable<[], FileContentResult>("get_launc // Updated config function using centralized configuration export const updateLsfgConfig = callable< - [string, number, number, boolean, boolean, string, number, boolean, boolean], + [string, number, number, boolean, boolean, string, number, boolean, boolean, boolean, boolean], ConfigUpdateResult >("update_lsfg_config"); // Helper function to create config update from configuration object export const updateLsfgConfigFromObject = async (config: ConfigurationData): Promise => { const args = ConfigurationManager.createArgsFromConfig(config); - return updateLsfgConfig(...args as [string, number, number, boolean, boolean, string, number, boolean, boolean]); + return updateLsfgConfig(...args as [string, number, number, boolean, boolean, string, number, boolean, boolean, boolean, boolean]); }; // Self-updater API functions diff --git a/src/components/ConfigurationSection.tsx b/src/components/ConfigurationSection.tsx index 59ad880..26099e7 100644 --- a/src/components/ConfigurationSection.tsx +++ b/src/components/ConfigurationSection.tsx @@ -154,6 +154,24 @@ export function ConfigurationSection({ onChange={(value) => onConfigChange('disable_steamdeck_mode', value)} /> + + + onConfigChange('mangohud_workaround', value)} + /> + + + + onConfigChange('disable_vkbasalt', value)} + /> + ); } diff --git a/src/config/configSchema.ts b/src/config/configSchema.ts index 4ab0d25..979d3c9 100644 --- a/src/config/configSchema.ts +++ b/src/config/configSchema.ts @@ -52,7 +52,7 @@ export class ConfigurationManager { /** * Create args array from config object for lsfg API calls */ - static createArgsFromConfig(config: ConfigurationData): [string, number, number, boolean, boolean, string, number, boolean, boolean] { + static createArgsFromConfig(config: ConfigurationData): [string, number, number, boolean, boolean, string, number, boolean, boolean, boolean, boolean] { return [ config.dll, config.multiplier, @@ -62,7 +62,9 @@ export class ConfigurationManager { config.experimental_present_mode, config.dxvk_frame_rate, config.enable_wow64, - config.disable_steamdeck_mode + config.disable_steamdeck_mode, + config.mangohud_workaround, + config.disable_vkbasalt ]; } diff --git a/src/config/generatedConfigSchema.ts b/src/config/generatedConfigSchema.ts index 5f0e368..cb08252 100644 --- a/src/config/generatedConfigSchema.ts +++ b/src/config/generatedConfigSchema.ts @@ -71,6 +71,18 @@ export const CONFIG_SCHEMA: Record = { default: false, description: "disable Steam Deck mode (unlocks hidden settings in some games)" }, + mangohud_workaround: { + name: "mangohud_workaround", + fieldType: ConfigFieldType.BOOLEAN, + default: false, + description: "Enables a transparent mangohud overlay, sometimes fixes issues with 2X multiplier in game mode" + }, + disable_vkbasalt: { + name: "disable_vkbasalt", + fieldType: ConfigFieldType.BOOLEAN, + default: false, + description: "Disables vkBasalt layer which can conflict with LSFG (Reshade, some Decky plugins)" + }, }; // Type-safe configuration data structure @@ -84,6 +96,8 @@ export interface ConfigurationData { dxvk_frame_rate: number; enable_wow64: boolean; disable_steamdeck_mode: boolean; + mangohud_workaround: boolean; + disable_vkbasalt: boolean; } // Helper functions @@ -102,6 +116,8 @@ export function getDefaults(): ConfigurationData { dxvk_frame_rate: 0, enable_wow64: false, disable_steamdeck_mode: false, + mangohud_workaround: false, + disable_vkbasalt: false, }; } @@ -116,6 +132,8 @@ export function getFieldTypes(): Record { dxvk_frame_rate: ConfigFieldType.INTEGER, enable_wow64: ConfigFieldType.BOOLEAN, disable_steamdeck_mode: ConfigFieldType.BOOLEAN, + mangohud_workaround: ConfigFieldType.BOOLEAN, + disable_vkbasalt: ConfigFieldType.BOOLEAN, }; } diff --git a/tests/conftest.py b/tests/conftest.py deleted file mode 100644 index 9ac31a0..0000000 --- a/tests/conftest.py +++ /dev/null @@ -1,31 +0,0 @@ -""" -Test configuration for the lsfg-vk plugin tests. -""" - -import pytest -from unittest.mock import Mock - - -@pytest.fixture -def mock_logger(): - """Provide a mock logger for testing""" - return Mock() - - -@pytest.fixture -def mock_decky_logger(monkeypatch): - """Mock decky.logger for tests that import decky""" - mock_logger = Mock() - - # Create a mock decky module - mock_decky = Mock() - mock_decky.logger = mock_logger - - # Monkeypatch the import - monkeypatch.setattr('lsfg_vk.base_service.decky', mock_decky) - monkeypatch.setattr('lsfg_vk.installation.decky', mock_decky) - monkeypatch.setattr('lsfg_vk.dll_detection.decky', mock_decky) - monkeypatch.setattr('lsfg_vk.configuration.decky', mock_decky) - monkeypatch.setattr('lsfg_vk.plugin.decky', mock_decky) - - return mock_logger diff --git a/tests/test_configuration.py b/tests/test_configuration.py deleted file mode 100644 index 3e0ad79..0000000 --- a/tests/test_configuration.py +++ /dev/null @@ -1,190 +0,0 @@ -""" -Tests for the configuration service. -""" - -import tempfile -from pathlib import Path -from unittest.mock import Mock - -from lsfg_vk.configuration import ConfigurationService -from lsfg_vk.config_schema import ConfigurationManager - - -def test_parse_script_content(): - """Test parsing of script content with current environment variable format""" - - # Test script content matching current format - script_content = """#!/bin/bash -# lsfg-vk launch script generated by decky-lossless-scaling-vk plugin -# This script sets up the environment for lsfg-vk to work with the plugin configuration -export PROTON_USE_WOW64=1 -export SteamDeck=0 -export DXVK_FRAME_RATE=18 -export LSFG_PROCESS=decky-lsfg-vk -exec "$@" -""" - - script_values = ConfigurationManager.parse_script_content(script_content) - - assert script_values["enable_wow64"] is True - assert script_values["disable_steamdeck_mode"] is True # SteamDeck=0 means disable - assert script_values["dxvk_frame_rate"] == 18 - - -def test_parse_script_content_minimal(): - """Test parsing when only required exports are present""" - - script_content = """#!/bin/bash -# lsfg-vk launch script generated by decky-lossless-scaling-vk plugin -export LSFG_PROCESS=decky-lsfg-vk -exec "$@" -""" - - script_values = ConfigurationManager.parse_script_content(script_content) - - # Should be empty dict since no tracked env vars are present - assert script_values == {} - - -def test_merge_config_with_script(): - """Test merging TOML config with script environment variables""" - - # Get defaults - toml_config = ConfigurationManager.get_defaults() - - # Script values from parsing - script_values = { - "enable_wow64": True, - "disable_steamdeck_mode": True, - "dxvk_frame_rate": 30 - } - - merged = ConfigurationManager.merge_config_with_script(toml_config, script_values) - - # TOML fields should be preserved - assert merged["multiplier"] == 1 # default from TOML - assert merged["flow_scale"] == 0.8 # default from TOML - assert merged["performance_mode"] is True # default from TOML - - # Script fields should be overlaid - assert merged["enable_wow64"] is True - assert merged["disable_steamdeck_mode"] is True - assert merged["dxvk_frame_rate"] == 30 - - with tempfile.TemporaryDirectory() as temp_dir: - temp_home = Path(temp_dir) - - service = ConfigurationService(logger=mock_logger) - service.user_home = temp_home - service.lsfg_script_path = temp_home / "lsfg" - - script_content = """#!/bin/bash - -# export ENABLE_LSFG=1 -export LSFG_MULTIPLIER=2 -export LSFG_FLOW_SCALE=1.0 -# export LSFG_HDR=1 -# export LSFG_PERF_MODE=1 -# export MESA_VK_WSI_PRESENT_MODE=immediate # - disable vsync - -exec "$@" -""" - - config = service._parse_script_content(script_content) - - assert config["enable_lsfg"] is False - assert config["multiplier"] == 2 - assert config["flow_scale"] == 1.0 - assert config["hdr"] is False - assert config["perf_mode"] is False - assert config["immediate_mode"] is False - - -def test_generate_script_content(): - """Test script content generation""" - mock_logger = Mock() - - with tempfile.TemporaryDirectory() as temp_dir: - temp_home = Path(temp_dir) - - service = ConfigurationService(logger=mock_logger) - service.user_home = temp_home - service.lsfg_script_path = temp_home / "lsfg" - - # Test with no toggles enabled - config = { - "enable_wow64": False, - "disable_steamdeck_mode": False - } - content = service._generate_script_content(config) - - assert "#!/bin/bash" in content - assert "export LSFG_PROCESS=decky-lsfg-vk" in content - assert "export PROTON_USE_WOW64=1" not in content - assert "export SteamDeck=0" not in content - assert 'exec "$@"' in content - - # Test with both toggles enabled - config = { - "enable_wow64": True, - "disable_steamdeck_mode": True - } - content = service._generate_script_content(config) - - assert "#!/bin/bash" in content - assert "export PROTON_USE_WOW64=1" in content - assert "export SteamDeck=0" in content - assert "export LSFG_PROCESS=decky-lsfg-vk" in content - assert 'exec "$@"' in content - - -def test_config_roundtrip(): - """Test that we can write config and read it back correctly""" - mock_logger = Mock() - - with tempfile.TemporaryDirectory() as temp_dir: - temp_home = Path(temp_dir) - - service = ConfigurationService(logger=mock_logger) - service.user_home = temp_home - service.lsfg_script_path = temp_home / "lsfg" - - # Update config - result = service.update_config( - dll="/path/to/dll", - multiplier=3, - flow_scale=1.5, - performance_mode=False, - hdr_mode=True, - experimental_present_mode="immediate", - dxvk_frame_rate=30, - enable_wow64=True, - disable_steamdeck_mode=False - ) - - assert result["success"] is True - - # Read it back - read_result = service.get_config() - - assert read_result["success"] is True - config = read_result["config"] - assert config["dll"] == "/path/to/dll" - assert config["multiplier"] == 3 - assert config["flow_scale"] == 1.5 - assert config["performance_mode"] is False - assert config["hdr_mode"] is True - assert config["experimental_present_mode"] == "immediate" - assert config["dxvk_frame_rate"] == 30 - assert config["enable_wow64"] is True - assert config["disable_steamdeck_mode"] is False - - assert read_result["success"] is True - config = read_result["config"] - - assert config["enable_lsfg"] is True - assert config["multiplier"] == 3 - assert config["flow_scale"] == 1.5 - assert config["hdr"] is True - assert config["perf_mode"] is False - assert config["immediate_mode"] is True diff --git a/tests/test_dll_detection.py b/tests/test_dll_detection.py deleted file mode 100644 index e50d733..0000000 --- a/tests/test_dll_detection.py +++ /dev/null @@ -1,129 +0,0 @@ -""" -Tests for the DLL detection service. -""" - -import os -import tempfile -from pathlib import Path -from unittest.mock import Mock, patch - -from lsfg_vk.dll_detection import DllDetectionService -from lsfg_vk.constants import LOSSLESS_DLL_NAME - - -def test_dll_detection_via_env_variable(): - """Test DLL detection via LSFG_DLL_PATH environment variable""" - mock_logger = Mock() - - with tempfile.TemporaryDirectory() as temp_dir: - # Create a fake DLL file - dll_path = Path(temp_dir) / LOSSLESS_DLL_NAME - dll_path.write_text("fake dll content") - - service = DllDetectionService(logger=mock_logger) - - # Test with environment variable set - with patch.dict(os.environ, {"LSFG_DLL_PATH": str(dll_path)}): - result = service.check_lossless_scaling_dll() - - assert result["detected"] is True - assert result["path"] == str(dll_path) - assert "LSFG_DLL_PATH" in result["source"] - assert result["error"] is None - - -def test_dll_detection_via_xdg_data_home(): - """Test DLL detection via XDG_DATA_HOME""" - mock_logger = Mock() - - with tempfile.TemporaryDirectory() as temp_dir: - # Create the expected directory structure - steam_dir = Path(temp_dir) / "Steam" / "steamapps" / "common" / "Lossless Scaling" - steam_dir.mkdir(parents=True) - - dll_path = steam_dir / LOSSLESS_DLL_NAME - dll_path.write_text("fake dll content") - - service = DllDetectionService(logger=mock_logger) - - # Test with XDG_DATA_HOME set, no LSFG_DLL_PATH - with patch.dict(os.environ, {"XDG_DATA_HOME": temp_dir}, clear=True): - result = service.check_lossless_scaling_dll() - - assert result["detected"] is True - assert result["path"] == str(dll_path) - assert "XDG_DATA_HOME" in result["source"] - assert result["error"] is None - - -def test_dll_detection_via_home_local_share(): - """Test DLL detection via HOME/.local/share""" - mock_logger = Mock() - - with tempfile.TemporaryDirectory() as temp_dir: - # Create the expected directory structure - steam_dir = Path(temp_dir) / ".local" / "share" / "Steam" / "steamapps" / "common" / "Lossless Scaling" - steam_dir.mkdir(parents=True) - - dll_path = steam_dir / LOSSLESS_DLL_NAME - dll_path.write_text("fake dll content") - - service = DllDetectionService(logger=mock_logger) - - # Test with HOME set, no other env vars - env = {"HOME": temp_dir} - with patch.dict(os.environ, env, clear=True): - result = service.check_lossless_scaling_dll() - - assert result["detected"] is True - assert result["path"] == str(dll_path) - assert "HOME/.local/share" in result["source"] - assert result["error"] is None - - -def test_dll_detection_not_found(): - """Test DLL detection when DLL is not found""" - mock_logger = Mock() - - service = DllDetectionService(logger=mock_logger) - - # Test with no environment variables set - with patch.dict(os.environ, {}, clear=True): - result = service.check_lossless_scaling_dll() - - assert result["detected"] is False - assert result["path"] is None - assert result["source"] is None - assert "not found" in result["message"] - assert result["error"] is None - - -def test_dll_detection_priority(): - """Test that LSFG_DLL_PATH takes priority over other locations""" - mock_logger = Mock() - - with tempfile.TemporaryDirectory() as temp_dir1, tempfile.TemporaryDirectory() as temp_dir2: - # Create DLL in both locations - dll_path1 = Path(temp_dir1) / LOSSLESS_DLL_NAME - dll_path1.write_text("fake dll content 1") - - steam_dir = Path(temp_dir2) / "Steam" / "steamapps" / "common" / "Lossless Scaling" - steam_dir.mkdir(parents=True) - dll_path2 = steam_dir / LOSSLESS_DLL_NAME - dll_path2.write_text("fake dll content 2") - - service = DllDetectionService(logger=mock_logger) - - # Set both environment variables - env = { - "LSFG_DLL_PATH": str(dll_path1), - "XDG_DATA_HOME": temp_dir2 - } - - with patch.dict(os.environ, env, clear=True): - result = service.check_lossless_scaling_dll() - - # Should prefer LSFG_DLL_PATH - assert result["detected"] is True - assert result["path"] == str(dll_path1) - assert "LSFG_DLL_PATH" in result["source"] diff --git a/tests/test_installation.py b/tests/test_installation.py deleted file mode 100644 index 2b3690e..0000000 --- a/tests/test_installation.py +++ /dev/null @@ -1,150 +0,0 @@ -""" -Tests for the installation service. -""" - -import os -import tempfile -import zipfile -from pathlib import Path -from unittest.mock import Mock, patch - -import pytest -from pyfakefs.fake_filesystem_unittest import TestCase - -from lsfg_vk.installation import InstallationService -from lsfg_vk.constants import LIB_FILENAME, JSON_FILENAME, ZIP_FILENAME - - -class TestInstallationService(TestCase): - """Test cases for InstallationService using pyfakefs""" - - def setUp(self): - """Set up fake filesystem""" - self.setUpPyfakefs() - self.mock_logger = Mock() - - # Create a test home directory - self.test_home = Path("/home/testuser") - self.fs.create_dir(self.test_home) - - # Patch Path.home() to return our test home - with patch('lsfg_vk.base_service.Path.home', return_value=self.test_home): - self.service = InstallationService(logger=self.mock_logger) - - def test_check_installation_no_files(self): - """Test installation check when no files are installed""" - result = self.service.check_installation() - - assert result["installed"] is False - assert result["lib_exists"] is False - assert result["json_exists"] is False - assert result["script_exists"] is False - assert result["error"] is None - - def test_check_installation_all_files_exist(self): - """Test installation check when all files exist""" - # Create the files - self.service.lib_file.parent.mkdir(parents=True, exist_ok=True) - self.service.lib_file.touch() - - self.service.json_file.parent.mkdir(parents=True, exist_ok=True) - self.service.json_file.touch() - - self.service.lsfg_script_path.touch() - - result = self.service.check_installation() - - assert result["installed"] is True - assert result["lib_exists"] is True - assert result["json_exists"] is True - assert result["script_exists"] is True - assert result["error"] is None - - def test_create_zip_for_testing(self): - """Helper to create a test zip file""" - # Create temp directory for zip contents - zip_content_dir = Path("/tmp/zip_content") - self.fs.create_dir(zip_content_dir) - - # Create test files - lib_file = zip_content_dir / LIB_FILENAME - json_file = zip_content_dir / JSON_FILENAME - - lib_file.write_text("fake library content") - json_file.write_text('{"layer": {"name": "VK_LAYER_LS_frame_generation"}}') - - # Create zip file - zip_path = Path("/tmp/test.zip") - with zipfile.ZipFile(zip_path, 'w') as zip_file: - zip_file.write(lib_file, LIB_FILENAME) - zip_file.write(json_file, JSON_FILENAME) - - return zip_path - - @patch('lsfg_vk.installation.Path.home') - def test_install_success(self, mock_home): - """Test successful installation""" - mock_home.return_value = self.test_home - - # Create the plugin directory and zip file - plugin_dir = Path("/plugin") - bin_dir = plugin_dir / "bin" - self.fs.create_dir(bin_dir) - - # Create a test zip file - zip_path = self.test_create_zip_for_testing() - zip_dest = bin_dir / ZIP_FILENAME - - # Copy our test zip to the expected location - with open(zip_path, 'rb') as src, open(zip_dest, 'wb') as dst: - dst.write(src.read()) - - # Mock the plugin directory detection - with patch('lsfg_vk.installation.Path.__file__', f"{plugin_dir}/lsfg_vk/installation.py"): - result = self.service.install() - - assert result["success"] is True - assert "successfully" in result["message"] - assert result["error"] is None - - # Check that files were created - assert self.service.lib_file.exists() - assert self.service.json_file.exists() - assert self.service.lsfg_script_path.exists() - - def test_uninstall_no_files(self): - """Test uninstall when no files exist""" - result = self.service.uninstall() - - assert result["success"] is True - assert "No lsfg-vk files found" in result["message"] - assert result["removed_files"] is None - - def test_uninstall_with_files(self): - """Test uninstall when files exist""" - # Create the files - self.service.lib_file.parent.mkdir(parents=True, exist_ok=True) - self.service.lib_file.touch() - - self.service.json_file.parent.mkdir(parents=True, exist_ok=True) - self.service.json_file.touch() - - self.service.lsfg_script_path.touch() - - result = self.service.uninstall() - - assert result["success"] is True - assert "uninstalled successfully" in result["message"] - assert len(result["removed_files"]) == 3 - - # Check that files were removed - assert not self.service.lib_file.exists() - assert not self.service.json_file.exists() - assert not self.service.lsfg_script_path.exists() - - -def test_installation_service_with_mock_logger(): - """Test that InstallationService accepts a mock logger""" - mock_logger = Mock() - service = InstallationService(logger=mock_logger) - assert service.log == mock_logger -- cgit v1.2.3 From dfe4c033dd1922a63c8393ab467e9aa58fa757e4 Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Tue, 22 Jul 2025 11:41:49 -0400 Subject: refactor: update configuration handling to use object-based API --- justfile | 7 +- py_modules/lsfg_vk/configuration.py | 2 +- py_modules/lsfg_vk/plugin.py | 40 ++++------ scripts/generate_ts_schema.py | 135 ++++++++++++++++++++++++++++++++ src/api/lsfgApi.ts | 11 ++- src/components/ConfigurationSection.tsx | 26 ++++-- src/config/configSchema.ts | 23 +----- 7 files changed, 183 insertions(+), 61 deletions(-) create mode 100644 scripts/generate_ts_schema.py diff --git a/justfile b/justfile index 9923416..054c113 100644 --- a/justfile +++ b/justfile @@ -1,8 +1,11 @@ default: - echo "Available recipes: build, test, clean" + echo "Available recipes: build, test, clean, generate-schema" + +generate-schema: + python3 scripts/generate_ts_schema.py build: - sudo rm -rf node_modules && .vscode/build.sh + python3 scripts/generate_ts_schema.py && sudo rm -rf node_modules && .vscode/build.sh test: scp "out/Lossless Scaling.zip" deck@192.168.0.6:~/Desktop diff --git a/py_modules/lsfg_vk/configuration.py b/py_modules/lsfg_vk/configuration.py index 82982e5..68ff577 100644 --- a/py_modules/lsfg_vk/configuration.py +++ b/py_modules/lsfg_vk/configuration.py @@ -222,7 +222,7 @@ class ConfigurationService(BaseService): if config.get("mangohud_workaround", False): lines.append("export MANGOHUD=1") - lines.append("export MANGOHUD_CONFIG=alpha=0.01,background_alpha=0.01") + lines.append("export MANGOHUD_CONFIG=alpha=0.001,background_alpha=0.001") if config.get("disable_vkbasalt", False): lines.append("export DISABLE_VKBASALT=1") diff --git a/py_modules/lsfg_vk/plugin.py b/py_modules/lsfg_vk/plugin.py index 7deed71..d126d84 100644 --- a/py_modules/lsfg_vk/plugin.py +++ b/py_modules/lsfg_vk/plugin.py @@ -184,36 +184,30 @@ class Plugin: "defaults": ConfigurationManager.get_defaults() } - async def update_lsfg_config(self, dll: str, multiplier: int, flow_scale: float, - performance_mode: bool, hdr_mode: bool, - experimental_present_mode: str = "fifo", - dxvk_frame_rate: int = 0, - enable_wow64: bool = False, - disable_steamdeck_mode: bool = False, - mangohud_workaround: bool = False, - disable_vkbasalt: bool = False) -> Dict[str, Any]: - """Update lsfg TOML configuration + async def update_lsfg_config(self, config: Dict[str, Any]) -> Dict[str, Any]: + """Update lsfg TOML configuration using object-based API Args: - dll: Path to Lossless.dll - multiplier: LSFG multiplier value - flow_scale: LSFG flow scale value - performance_mode: Whether to enable performance mode - hdr_mode: Whether to enable HDR mode - experimental_present_mode: Experimental Vulkan present mode override - dxvk_frame_rate: Frame rate cap for DirectX games, before frame multiplier (0 = disabled) - enable_wow64: Whether to enable PROTON_USE_WOW64=1 for 32-bit games - disable_steamdeck_mode: Whether to disable Steam Deck mode - mangohud_workaround: Whether to enable MangoHud workaround with transparent overlay - disable_vkbasalt: Whether to disable vkBasalt layer + config: Configuration data dictionary containing all settings Returns: ConfigurationResponse dict with success status """ + # Validate and extract configuration from the config dict + validated_config = ConfigurationManager.validate_config(config) + return self.configuration_service.update_config( - dll, multiplier, flow_scale, performance_mode, hdr_mode, - experimental_present_mode, dxvk_frame_rate, enable_wow64, disable_steamdeck_mode, - mangohud_workaround, disable_vkbasalt + dll=validated_config["dll"], + multiplier=validated_config["multiplier"], + flow_scale=validated_config["flow_scale"], + performance_mode=validated_config["performance_mode"], + hdr_mode=validated_config["hdr_mode"], + experimental_present_mode=validated_config["experimental_present_mode"], + dxvk_frame_rate=validated_config["dxvk_frame_rate"], + enable_wow64=validated_config["enable_wow64"], + disable_steamdeck_mode=validated_config["disable_steamdeck_mode"], + mangohud_workaround=validated_config["mangohud_workaround"], + disable_vkbasalt=validated_config["disable_vkbasalt"] ) async def update_dll_path(self, dll_path: str) -> Dict[str, Any]: diff --git a/scripts/generate_ts_schema.py b/scripts/generate_ts_schema.py new file mode 100644 index 0000000..dcdddf2 --- /dev/null +++ b/scripts/generate_ts_schema.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 +""" +Generate TypeScript schema from Python shared_config.py + +This script reads the canonical schema from shared_config.py and generates +the corresponding TypeScript files, ensuring single source of truth. +""" + +import sys +from pathlib import Path + +# Add project root to path to import shared_config +project_root = Path(__file__).parent.parent +sys.path.insert(0, str(project_root)) + +from shared_config import CONFIG_SCHEMA_DEF, ConfigFieldType + + +def generate_typescript_schema(): + """Generate generatedConfigSchema.ts from Python schema""" + + # Generate enum + enum_lines = [ + "// src/config/generatedConfigSchema.ts", + "// Configuration field type enum - matches Python", + "export enum ConfigFieldType {", + " BOOLEAN = \"boolean\",", + " INTEGER = \"integer\",", + " FLOAT = \"float\",", + " STRING = \"string\"", + "}", + "", + "// Configuration field definition", + "export interface ConfigField {", + " name: string;", + " fieldType: ConfigFieldType;", + " default: boolean | number | string;", + " description: string;", + "}", + "", + "// Configuration schema - auto-generated from Python", + "export const CONFIG_SCHEMA: Record = {" + ] + + # Generate schema entries + schema_entries = [] + interface_fields = [] + defaults_fields = [] + field_types = [] + + for field_name, field_def in CONFIG_SCHEMA_DEF.items(): + # Schema entry + default_value = field_def["default"] + if isinstance(default_value, str): + default_str = f'"{default_value}"' + elif isinstance(default_value, bool): + default_str = "true" if default_value else "false" + else: + default_str = str(default_value) + + schema_entries.append(f' {field_name}: {{') + schema_entries.append(f' name: "{field_def["name"]}",') + schema_entries.append(f' fieldType: ConfigFieldType.{field_def["fieldType"].upper()},') + schema_entries.append(f' default: {default_str},') + schema_entries.append(f' description: "{field_def["description"]}"') + schema_entries.append(' },') + + # Interface field + if field_def["fieldType"] == ConfigFieldType.BOOLEAN: + ts_type = "boolean" + elif field_def["fieldType"] == ConfigFieldType.INTEGER: + ts_type = "number" + elif field_def["fieldType"] == ConfigFieldType.FLOAT: + ts_type = "number" + elif field_def["fieldType"] == ConfigFieldType.STRING: + ts_type = "string" + else: + ts_type = "any" + + interface_fields.append(f' {field_name}: {ts_type};') + defaults_fields.append(f' {field_name}: {default_str},') + field_types.append(f' {field_name}: ConfigFieldType.{field_def["fieldType"].upper()},') + + # Complete the file + all_lines = enum_lines + schema_entries + [ + "};", + "", + "// Type-safe configuration data structure", + "export interface ConfigurationData {", + ] + interface_fields + [ + "}", + "", + "// Helper functions", + "export function getFieldNames(): string[] {", + " return Object.keys(CONFIG_SCHEMA);", + "}", + "", + "export function getDefaults(): ConfigurationData {", + " return {", + ] + defaults_fields + [ + " };", + "}", + "", + "export function getFieldTypes(): Record {", + " return {", + ] + field_types + [ + " };", + "}", + "", + "" + ] + + return "\n".join(all_lines) + + +def main(): + """Main function to generate TypeScript schema""" + try: + # Generate the TypeScript content + ts_content = generate_typescript_schema() + + # Write to the target file + target_file = project_root / "src" / "config" / "generatedConfigSchema.ts" + target_file.write_text(ts_content) + + print(f"āœ… Generated {target_file} from shared_config.py") + print(f" Fields: {len(CONFIG_SCHEMA_DEF)}") + + except Exception as e: + print(f"āŒ Error generating TypeScript schema: {e}") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/src/api/lsfgApi.ts b/src/api/lsfgApi.ts index 0b221e9..8d14da6 100644 --- a/src/api/lsfgApi.ts +++ b/src/api/lsfgApi.ts @@ -1,5 +1,5 @@ import { callable } from "@decky/api"; -import { ConfigurationData, ConfigurationManager } from "../config/configSchema"; +import { ConfigurationData } from "../config/configSchema"; // Type definitions for API responses export interface InstallationResult { @@ -99,16 +99,15 @@ export const getLaunchOption = callable<[], LaunchOptionResult>("get_launch_opti export const getConfigFileContent = callable<[], FileContentResult>("get_config_file_content"); export const getLaunchScriptContent = callable<[], FileContentResult>("get_launch_script_content"); -// Updated config function using centralized configuration +// Updated config function using object-based configuration (single source of truth) export const updateLsfgConfig = callable< - [string, number, number, boolean, boolean, string, number, boolean, boolean, boolean, boolean], + [ConfigurationData], ConfigUpdateResult >("update_lsfg_config"); -// Helper function to create config update from configuration object +// Legacy helper function for backward compatibility export const updateLsfgConfigFromObject = async (config: ConfigurationData): Promise => { - const args = ConfigurationManager.createArgsFromConfig(config); - return updateLsfgConfig(...args as [string, number, number, boolean, boolean, string, number, boolean, boolean, boolean, boolean]); + return updateLsfgConfig(config); }; // Self-updater API functions diff --git a/src/components/ConfigurationSection.tsx b/src/components/ConfigurationSection.tsx index 26099e7..1c0d2b2 100644 --- a/src/components/ConfigurationSection.tsx +++ b/src/components/ConfigurationSection.tsx @@ -112,16 +112,26 @@ export function ConfigurationSection({
+ Environment Variables +
+
- Environment Variables (Requires re-launch) + Must be toggled before game start or restart game to take effect
diff --git a/src/config/configSchema.ts b/src/config/configSchema.ts index 979d3c9..fdf6212 100644 --- a/src/config/configSchema.ts +++ b/src/config/configSchema.ts @@ -8,6 +8,7 @@ import { callable } from "@decky/api"; import type { ConfigurationData } from './generatedConfigSchema'; import { getDefaults } from './generatedConfigSchema'; +import { updateLsfgConfig } from "../api/lsfgApi"; // Re-export all auto-generated configuration constants export { @@ -30,7 +31,6 @@ export class ConfigurationManager { // Callable methods for backend communication private getConfiguration = callable<[], { success: boolean; data?: ConfigurationData; error?: string }>("get_configuration"); - private setConfiguration = callable<[{ config_data: ConfigurationData }], { success: boolean; error?: string }>("set_configuration"); private resetConfiguration = callable<[], { success: boolean; data?: ConfigurationData; error?: string }>("reset_configuration"); private constructor() {} @@ -49,25 +49,6 @@ export class ConfigurationManager { return getDefaults(); } - /** - * Create args array from config object for lsfg API calls - */ - static createArgsFromConfig(config: ConfigurationData): [string, number, number, boolean, boolean, string, number, boolean, boolean, boolean, boolean] { - return [ - config.dll, - config.multiplier, - config.flow_scale, - config.performance_mode, - config.hdr_mode, - config.experimental_present_mode, - config.dxvk_frame_rate, - config.enable_wow64, - config.disable_steamdeck_mode, - config.mangohud_workaround, - config.disable_vkbasalt - ]; - } - /** * Load configuration from backend */ @@ -91,7 +72,7 @@ export class ConfigurationManager { */ async saveConfig(config: ConfigurationData): Promise { try { - const result = await this.setConfiguration({ config_data: config }); + const result = await updateLsfgConfig(config); if (result.success) { this._config = config; } else { -- cgit v1.2.3 From f8139896f2077a95a78a54c818637f78dd102de8 Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Tue, 22 Jul 2025 12:11:53 -0400 Subject: consolidate toml and script values --- py_modules/lsfg_vk/config_schema.py | 54 +++++++++++--------------------- py_modules/lsfg_vk/configuration.py | 55 +++++++++++++++++++++++++++++++-- py_modules/lsfg_vk/plugin.py | 17 ++-------- shared_config.py | 49 ++++++++++++++++++++++------- src/components/ConfigurationSection.tsx | 32 +++++++++++++++++++ src/config/generatedConfigSchema.ts | 18 +++++++++++ tests/test_configuration.py | 0 7 files changed, 161 insertions(+), 64 deletions(-) create mode 100644 tests/test_configuration.py diff --git a/py_modules/lsfg_vk/config_schema.py b/py_modules/lsfg_vk/config_schema.py index 6a68db1..eac5a91 100644 --- a/py_modules/lsfg_vk/config_schema.py +++ b/py_modules/lsfg_vk/config_schema.py @@ -52,42 +52,16 @@ CONFIG_SCHEMA["dll"] = ConfigField( description="specify where Lossless.dll is stored" ) -# Fields that should ONLY be in the lsfg script, not in TOML config +# Get script-only fields dynamically from shared config SCRIPT_ONLY_FIELDS = { - "dxvk_frame_rate": ConfigField( - name="dxvk_frame_rate", - field_type=ConfigFieldType.INTEGER, - default=0, - description="base framerate cap for DirectX games, before frame multiplier (0 = disabled, requires game re-launch)" - ), - - "enable_wow64": ConfigField( - name="enable_wow64", - field_type=ConfigFieldType.BOOLEAN, - default=False, - description="enable PROTON_USE_WOW64=1 for 32-bit games (use with ProtonGE to fix crashing)" - ), - - "disable_steamdeck_mode": ConfigField( - name="disable_steamdeck_mode", - field_type=ConfigFieldType.BOOLEAN, - default=False, - description="disable Steam Deck mode (unlocks hidden settings in some games)" - ), - - "mangohud_workaround": ConfigField( - name="mangohud_workaround", - field_type=ConfigFieldType.BOOLEAN, - default=False, - description="Enables a transparent mangohud overlay, sometimes fixes issues with 2X multiplier in game mode" - ), - - "disable_vkbasalt": ConfigField( - name="disable_vkbasalt", - field_type=ConfigFieldType.BOOLEAN, - default=False, - description="Disables vkBasalt layer which can conflict with LSFG (Reshade, some Decky plugins)" + field_name: ConfigField( + name=field_def["name"], + field_type=ConfigFieldType(field_def["fieldType"]), + default=field_def["default"], + description=field_def["description"] ) + for field_name, field_def in CONFIG_SCHEMA_DEF.items() + if field_def.get("location") == "script" } # Complete configuration schema (TOML + script-only fields) @@ -107,6 +81,8 @@ class ConfigurationData(TypedDict): disable_steamdeck_mode: bool mangohud_workaround: bool disable_vkbasalt: bool + foobar_toggle: bool + test_config_only: str class ConfigurationManager: @@ -355,6 +331,8 @@ class ConfigurationManager: script_values["mangohud_workaround"] = value == "1" elif key == "DISABLE_VKBASALT": script_values["disable_vkbasalt"] = value == "1" + elif key == "FOOBAR": + script_values["foobar_toggle"] = value == "1" except (ValueError, KeyError, IndexError) as e: # If parsing fails, log the error and return empty dict (will use defaults) @@ -390,7 +368,9 @@ class ConfigurationManager: enable_wow64: bool = False, disable_steamdeck_mode: bool = False, mangohud_workaround: bool = False, - disable_vkbasalt: bool = False) -> ConfigurationData: + disable_vkbasalt: bool = False, + foobar_toggle: bool = False, + test_config_only: str = "default_value") -> ConfigurationData: """Create configuration from individual arguments""" return cast(ConfigurationData, { "dll": dll, @@ -403,5 +383,7 @@ class ConfigurationManager: "enable_wow64": enable_wow64, "disable_steamdeck_mode": disable_steamdeck_mode, "mangohud_workaround": mangohud_workaround, - "disable_vkbasalt": disable_vkbasalt + "disable_vkbasalt": disable_vkbasalt, + "foobar_toggle": foobar_toggle, + "test_config_only": test_config_only }) diff --git a/py_modules/lsfg_vk/configuration.py b/py_modules/lsfg_vk/configuration.py index 68ff577..b4c7994 100644 --- a/py_modules/lsfg_vk/configuration.py +++ b/py_modules/lsfg_vk/configuration.py @@ -60,6 +60,47 @@ class ConfigurationService(BaseService): f"Using default configuration due to parse error: {str(e)}", config=config) + def update_config_from_dict(self, config: ConfigurationData) -> ConfigurationResponse: + """Update TOML configuration from configuration dictionary (eliminates parameter duplication) + + Args: + config: Complete configuration data dictionary + + Returns: + ConfigurationResponse with success status + """ + try: + # Generate TOML content using centralized manager + toml_content = ConfigurationManager.generate_toml_content(config) + + # Ensure config directory exists + self.config_dir.mkdir(parents=True, exist_ok=True) + + # Write the updated config directly to preserve inode for file watchers + self._write_file(self.config_file_path, toml_content, 0o644) + + # Update the launch script with the new configuration + script_result = self.update_lsfg_script(config) + if not script_result["success"]: + self.log.warning(f"Failed to update launch script: {script_result['error']}") + + # Log with dynamic field listing + field_values = ", ".join(f"{k}={repr(v)}" for k, v in config.items()) + self.log.info(f"Updated lsfg configuration: {field_values}") + + return self._success_response(ConfigurationResponse, + "lsfg configuration updated successfully", + config=config) + + except (OSError, IOError) as e: + error_msg = f"Error updating lsfg config: {str(e)}" + self.log.error(error_msg) + return self._error_response(ConfigurationResponse, str(e), config=None) + except ValueError as e: + error_msg = f"Invalid configuration arguments: {str(e)}" + self.log.error(error_msg) + return self._error_response(ConfigurationResponse, str(e), config=None) + def update_config(self, dll: str, multiplier: int, flow_scale: float, performance_mode: bool, hdr_mode: bool, experimental_present_mode: str = "fifo", @@ -67,7 +108,9 @@ class ConfigurationService(BaseService): enable_wow64: bool = False, disable_steamdeck_mode: bool = False, mangohud_workaround: bool = False, - disable_vkbasalt: bool = False) -> ConfigurationResponse: + disable_vkbasalt: bool = False, + foobar_toggle: bool = False, + test_config_only: str = "default_value") -> ConfigurationResponse: """Update TOML configuration Args: @@ -82,6 +125,8 @@ class ConfigurationService(BaseService): disable_steamdeck_mode: Whether to disable Steam Deck mode mangohud_workaround: Whether to enable MangoHud workaround with transparent overlay disable_vkbasalt: Whether to disable vkBasalt layer + foobar_toggle: Test script-only toggle that exports FOOBAR=1 + test_config_only: Test TOML-only configuration field Returns: ConfigurationResponse with success status @@ -91,7 +136,7 @@ class ConfigurationService(BaseService): config = ConfigurationManager.create_config_from_args( dll, multiplier, flow_scale, performance_mode, hdr_mode, experimental_present_mode, dxvk_frame_rate, enable_wow64, disable_steamdeck_mode, - mangohud_workaround, disable_vkbasalt + mangohud_workaround, disable_vkbasalt, foobar_toggle, test_config_only ) # Generate TOML content using centralized manager @@ -114,7 +159,8 @@ class ConfigurationService(BaseService): f"experimental_present_mode='{experimental_present_mode}', " f"dxvk_frame_rate={dxvk_frame_rate}, " f"enable_wow64={enable_wow64}, disable_steamdeck_mode={disable_steamdeck_mode}, " - f"mangohud_workaround={mangohud_workaround}, disable_vkbasalt={disable_vkbasalt}") + f"mangohud_workaround={mangohud_workaround}, disable_vkbasalt={disable_vkbasalt}, " + f"foobar_toggle={foobar_toggle}, test_config_only='{test_config_only}'") return self._success_response(ConfigurationResponse, "lsfg configuration updated successfully", @@ -227,6 +273,9 @@ class ConfigurationService(BaseService): if config.get("disable_vkbasalt", False): lines.append("export DISABLE_VKBASALT=1") + if config.get("foobar_toggle", False): + lines.append("export FOOBAR=1") + # Add DXVK_FRAME_RATE if dxvk_frame_rate is set dxvk_frame_rate = config.get("dxvk_frame_rate", 0) if dxvk_frame_rate > 0: diff --git a/py_modules/lsfg_vk/plugin.py b/py_modules/lsfg_vk/plugin.py index d126d84..8fa2435 100644 --- a/py_modules/lsfg_vk/plugin.py +++ b/py_modules/lsfg_vk/plugin.py @@ -185,7 +185,7 @@ class Plugin: } async def update_lsfg_config(self, config: Dict[str, Any]) -> Dict[str, Any]: - """Update lsfg TOML configuration using object-based API + """Update lsfg TOML configuration using object-based API (single source of truth) Args: config: Configuration data dictionary containing all settings @@ -196,19 +196,8 @@ class Plugin: # Validate and extract configuration from the config dict validated_config = ConfigurationManager.validate_config(config) - return self.configuration_service.update_config( - dll=validated_config["dll"], - multiplier=validated_config["multiplier"], - flow_scale=validated_config["flow_scale"], - performance_mode=validated_config["performance_mode"], - hdr_mode=validated_config["hdr_mode"], - experimental_present_mode=validated_config["experimental_present_mode"], - dxvk_frame_rate=validated_config["dxvk_frame_rate"], - enable_wow64=validated_config["enable_wow64"], - disable_steamdeck_mode=validated_config["disable_steamdeck_mode"], - mangohud_workaround=validated_config["mangohud_workaround"], - disable_vkbasalt=validated_config["disable_vkbasalt"] - ) + # Use dynamic parameter passing based on schema + return self.configuration_service.update_config_from_dict(validated_config) async def update_dll_path(self, dll_path: str) -> Dict[str, Any]: """Update the DLL path in the configuration when detected diff --git a/shared_config.py b/shared_config.py index 518294d..ca9172f 100644 --- a/shared_config.py +++ b/shared_config.py @@ -24,77 +24,104 @@ CONFIG_SCHEMA_DEF = { "name": "dll", "fieldType": ConfigFieldType.STRING, "default": "/games/Lossless Scaling/Lossless.dll", - "description": "specify where Lossless.dll is stored" + "description": "specify where Lossless.dll is stored", + "location": "toml" # where this field is stored/used }, "multiplier": { "name": "multiplier", "fieldType": ConfigFieldType.INTEGER, "default": 1, - "description": "change the fps multiplier" + "description": "change the fps multiplier", + "location": "toml" }, "flow_scale": { "name": "flow_scale", "fieldType": ConfigFieldType.FLOAT, "default": 0.8, - "description": "change the flow scale" + "description": "change the flow scale", + "location": "toml" }, "performance_mode": { "name": "performance_mode", "fieldType": ConfigFieldType.BOOLEAN, "default": True, - "description": "use a lighter model for FG (recommended for most games)" + "description": "use a lighter model for FG (recommended for most games)", + "location": "toml" }, "hdr_mode": { "name": "hdr_mode", "fieldType": ConfigFieldType.BOOLEAN, "default": False, - "description": "enable HDR mode (only for games that support HDR)" + "description": "enable HDR mode (only for games that support HDR)", + "location": "toml" }, "experimental_present_mode": { "name": "experimental_present_mode", "fieldType": ConfigFieldType.STRING, "default": "fifo", - "description": "override Vulkan present mode (may cause crashes)" + "description": "override Vulkan present mode (may cause crashes)", + "location": "toml" }, "dxvk_frame_rate": { "name": "dxvk_frame_rate", "fieldType": ConfigFieldType.INTEGER, "default": 0, - "description": "base framerate cap for DirectX games before frame multiplier" + "description": "base framerate cap for DirectX games before frame multiplier", + "location": "script" # script-only field }, "enable_wow64": { "name": "enable_wow64", "fieldType": ConfigFieldType.BOOLEAN, "default": False, - "description": "enable PROTON_USE_WOW64=1 for 32-bit games (use with ProtonGE to fix crashing)" + "description": "enable PROTON_USE_WOW64=1 for 32-bit games (use with ProtonGE to fix crashing)", + "location": "script" }, "disable_steamdeck_mode": { "name": "disable_steamdeck_mode", "fieldType": ConfigFieldType.BOOLEAN, "default": False, - "description": "disable Steam Deck mode (unlocks hidden settings in some games)" + "description": "disable Steam Deck mode (unlocks hidden settings in some games)", + "location": "script" }, "mangohud_workaround": { "name": "mangohud_workaround", "fieldType": ConfigFieldType.BOOLEAN, "default": False, - "description": "Enables a transparent mangohud overlay, sometimes fixes issues with 2X multiplier in game mode" + "description": "Enables a transparent mangohud overlay, sometimes fixes issues with 2X multiplier in game mode", + "location": "script" }, "disable_vkbasalt": { "name": "disable_vkbasalt", "fieldType": ConfigFieldType.BOOLEAN, "default": False, - "description": "Disables vkBasalt layer which can conflict with LSFG (Reshade, some Decky plugins)" + "description": "Disables vkBasalt layer which can conflict with LSFG (Reshade, some Decky plugins)", + "location": "script" + }, + + "foobar_toggle": { + "name": "foobar_toggle", + "fieldType": ConfigFieldType.BOOLEAN, + "default": False, + "description": "Test script-only toggle that exports FOOBAR=1 (for testing purposes)", + "location": "script" + }, + + "test_config_only": { + "name": "test_config_only", + "fieldType": ConfigFieldType.STRING, + "default": "default_value", + "description": "Test TOML-only configuration field (not in script)", + "location": "toml" } } diff --git a/src/components/ConfigurationSection.tsx b/src/components/ConfigurationSection.tsx index 1c0d2b2..3f15bac 100644 --- a/src/components/ConfigurationSection.tsx +++ b/src/components/ConfigurationSection.tsx @@ -182,6 +182,38 @@ export function ConfigurationSection({ onChange={(value) => onConfigChange('disable_vkbasalt', value)} /> + + + onConfigChange('foobar_toggle', value)} + /> + + + +
+
Test Config Only Field
+ onConfigChange('test_config_only', e.target.value)} + placeholder="Enter test value" + style={{ + width: "100%", + padding: "8px", + borderRadius: "4px", + border: "1px solid #4c4c4c", + backgroundColor: "#2d2d2d", + color: "#ffffff" + }} + /> +
+ Test TOML-only configuration field (not in script) +
+
+
); } diff --git a/src/config/generatedConfigSchema.ts b/src/config/generatedConfigSchema.ts index cb08252..b7487bd 100644 --- a/src/config/generatedConfigSchema.ts +++ b/src/config/generatedConfigSchema.ts @@ -83,6 +83,18 @@ export const CONFIG_SCHEMA: Record = { default: false, description: "Disables vkBasalt layer which can conflict with LSFG (Reshade, some Decky plugins)" }, + foobar_toggle: { + name: "foobar_toggle", + fieldType: ConfigFieldType.BOOLEAN, + default: false, + description: "Test script-only toggle that exports FOOBAR=1 (for testing purposes)" + }, + test_config_only: { + name: "test_config_only", + fieldType: ConfigFieldType.STRING, + default: "default_value", + description: "Test TOML-only configuration field (not in script)" + }, }; // Type-safe configuration data structure @@ -98,6 +110,8 @@ export interface ConfigurationData { disable_steamdeck_mode: boolean; mangohud_workaround: boolean; disable_vkbasalt: boolean; + foobar_toggle: boolean; + test_config_only: string; } // Helper functions @@ -118,6 +132,8 @@ export function getDefaults(): ConfigurationData { disable_steamdeck_mode: false, mangohud_workaround: false, disable_vkbasalt: false, + foobar_toggle: false, + test_config_only: "default_value", }; } @@ -134,6 +150,8 @@ export function getFieldTypes(): Record { disable_steamdeck_mode: ConfigFieldType.BOOLEAN, mangohud_workaround: ConfigFieldType.BOOLEAN, disable_vkbasalt: ConfigFieldType.BOOLEAN, + foobar_toggle: ConfigFieldType.BOOLEAN, + test_config_only: ConfigFieldType.STRING, }; } diff --git a/tests/test_configuration.py b/tests/test_configuration.py new file mode 100644 index 0000000..e69de29 -- cgit v1.2.3 From f8c09209513507ad9af7822c32119cf6d6fae0ac Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Tue, 22 Jul 2025 12:40:23 -0400 Subject: rm test config options --- py_modules/lsfg_vk/config_schema.py | 12 ++---------- py_modules/lsfg_vk/configuration.py | 14 +++----------- shared_config.py | 16 ---------------- src/components/ConfigurationSection.tsx | 32 -------------------------------- 4 files changed, 5 insertions(+), 69 deletions(-) diff --git a/py_modules/lsfg_vk/config_schema.py b/py_modules/lsfg_vk/config_schema.py index eac5a91..6728106 100644 --- a/py_modules/lsfg_vk/config_schema.py +++ b/py_modules/lsfg_vk/config_schema.py @@ -81,8 +81,6 @@ class ConfigurationData(TypedDict): disable_steamdeck_mode: bool mangohud_workaround: bool disable_vkbasalt: bool - foobar_toggle: bool - test_config_only: str class ConfigurationManager: @@ -331,8 +329,6 @@ class ConfigurationManager: script_values["mangohud_workaround"] = value == "1" elif key == "DISABLE_VKBASALT": script_values["disable_vkbasalt"] = value == "1" - elif key == "FOOBAR": - script_values["foobar_toggle"] = value == "1" except (ValueError, KeyError, IndexError) as e: # If parsing fails, log the error and return empty dict (will use defaults) @@ -368,9 +364,7 @@ class ConfigurationManager: enable_wow64: bool = False, disable_steamdeck_mode: bool = False, mangohud_workaround: bool = False, - disable_vkbasalt: bool = False, - foobar_toggle: bool = False, - test_config_only: str = "default_value") -> ConfigurationData: + disable_vkbasalt: bool = False) -> ConfigurationData: """Create configuration from individual arguments""" return cast(ConfigurationData, { "dll": dll, @@ -383,7 +377,5 @@ class ConfigurationManager: "enable_wow64": enable_wow64, "disable_steamdeck_mode": disable_steamdeck_mode, "mangohud_workaround": mangohud_workaround, - "disable_vkbasalt": disable_vkbasalt, - "foobar_toggle": foobar_toggle, - "test_config_only": test_config_only + "disable_vkbasalt": disable_vkbasalt }) diff --git a/py_modules/lsfg_vk/configuration.py b/py_modules/lsfg_vk/configuration.py index b4c7994..49653ea 100644 --- a/py_modules/lsfg_vk/configuration.py +++ b/py_modules/lsfg_vk/configuration.py @@ -108,9 +108,7 @@ class ConfigurationService(BaseService): enable_wow64: bool = False, disable_steamdeck_mode: bool = False, mangohud_workaround: bool = False, - disable_vkbasalt: bool = False, - foobar_toggle: bool = False, - test_config_only: str = "default_value") -> ConfigurationResponse: + disable_vkbasalt: bool = False) -> ConfigurationResponse: """Update TOML configuration Args: @@ -125,8 +123,6 @@ class ConfigurationService(BaseService): disable_steamdeck_mode: Whether to disable Steam Deck mode mangohud_workaround: Whether to enable MangoHud workaround with transparent overlay disable_vkbasalt: Whether to disable vkBasalt layer - foobar_toggle: Test script-only toggle that exports FOOBAR=1 - test_config_only: Test TOML-only configuration field Returns: ConfigurationResponse with success status @@ -136,7 +132,7 @@ class ConfigurationService(BaseService): config = ConfigurationManager.create_config_from_args( dll, multiplier, flow_scale, performance_mode, hdr_mode, experimental_present_mode, dxvk_frame_rate, enable_wow64, disable_steamdeck_mode, - mangohud_workaround, disable_vkbasalt, foobar_toggle, test_config_only + mangohud_workaround, disable_vkbasalt ) # Generate TOML content using centralized manager @@ -159,8 +155,7 @@ class ConfigurationService(BaseService): f"experimental_present_mode='{experimental_present_mode}', " f"dxvk_frame_rate={dxvk_frame_rate}, " f"enable_wow64={enable_wow64}, disable_steamdeck_mode={disable_steamdeck_mode}, " - f"mangohud_workaround={mangohud_workaround}, disable_vkbasalt={disable_vkbasalt}, " - f"foobar_toggle={foobar_toggle}, test_config_only='{test_config_only}'") + f"mangohud_workaround={mangohud_workaround}, disable_vkbasalt={disable_vkbasalt}") return self._success_response(ConfigurationResponse, "lsfg configuration updated successfully", @@ -273,9 +268,6 @@ class ConfigurationService(BaseService): if config.get("disable_vkbasalt", False): lines.append("export DISABLE_VKBASALT=1") - if config.get("foobar_toggle", False): - lines.append("export FOOBAR=1") - # Add DXVK_FRAME_RATE if dxvk_frame_rate is set dxvk_frame_rate = config.get("dxvk_frame_rate", 0) if dxvk_frame_rate > 0: diff --git a/shared_config.py b/shared_config.py index ca9172f..68c7b82 100644 --- a/shared_config.py +++ b/shared_config.py @@ -106,22 +106,6 @@ CONFIG_SCHEMA_DEF = { "default": False, "description": "Disables vkBasalt layer which can conflict with LSFG (Reshade, some Decky plugins)", "location": "script" - }, - - "foobar_toggle": { - "name": "foobar_toggle", - "fieldType": ConfigFieldType.BOOLEAN, - "default": False, - "description": "Test script-only toggle that exports FOOBAR=1 (for testing purposes)", - "location": "script" - }, - - "test_config_only": { - "name": "test_config_only", - "fieldType": ConfigFieldType.STRING, - "default": "default_value", - "description": "Test TOML-only configuration field (not in script)", - "location": "toml" } } diff --git a/src/components/ConfigurationSection.tsx b/src/components/ConfigurationSection.tsx index 3f15bac..1c0d2b2 100644 --- a/src/components/ConfigurationSection.tsx +++ b/src/components/ConfigurationSection.tsx @@ -182,38 +182,6 @@ export function ConfigurationSection({ onChange={(value) => onConfigChange('disable_vkbasalt', value)} /> - - - onConfigChange('foobar_toggle', value)} - /> - - - -
-
Test Config Only Field
- onConfigChange('test_config_only', e.target.value)} - placeholder="Enter test value" - style={{ - width: "100%", - padding: "8px", - borderRadius: "4px", - border: "1px solid #4c4c4c", - backgroundColor: "#2d2d2d", - color: "#ffffff" - }} - /> -
- Test TOML-only configuration field (not in script) -
-
-
); } -- cgit v1.2.3 From df0635f1bba611b8b44975057acd579102d209dd Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Tue, 22 Jul 2025 13:06:46 -0400 Subject: further automate population of hardcoded fields --- py_modules/lsfg_vk/config_schema.py | 64 +---- py_modules/lsfg_vk/config_schema_generated.py | 118 ++++++++ py_modules/lsfg_vk/configuration.py | 44 +-- .../lsfg_vk/configuration_helpers_generated.py | 22 ++ scripts/generate_python_boilerplate.py | 319 +++++++++++++++++++++ scripts/generate_ts_schema.py | 18 +- src/config/generatedConfigSchema.ts | 18 -- 7 files changed, 499 insertions(+), 104 deletions(-) create mode 100644 py_modules/lsfg_vk/config_schema_generated.py create mode 100644 py_modules/lsfg_vk/configuration_helpers_generated.py create mode 100644 scripts/generate_python_boilerplate.py diff --git a/py_modules/lsfg_vk/config_schema.py b/py_modules/lsfg_vk/config_schema.py index 6728106..a7827ae 100644 --- a/py_modules/lsfg_vk/config_schema.py +++ b/py_modules/lsfg_vk/config_schema.py @@ -19,6 +19,9 @@ from pathlib import Path sys.path.insert(0, str(Path(__file__).parent.parent.parent)) from shared_config import CONFIG_SCHEMA_DEF, ConfigFieldType, get_field_names, get_defaults, get_field_types +# Import auto-generated configuration components +from .config_schema_generated import ConfigurationData, get_script_parsing_logic, get_script_generation_logic + @dataclass class ConfigField: @@ -68,19 +71,11 @@ SCRIPT_ONLY_FIELDS = { COMPLETE_CONFIG_SCHEMA = {**CONFIG_SCHEMA, **SCRIPT_ONLY_FIELDS} -class ConfigurationData(TypedDict): - """Type-safe configuration data structure""" - dll: str - multiplier: int - flow_scale: float - performance_mode: bool - hdr_mode: bool - experimental_present_mode: str - dxvk_frame_rate: int - enable_wow64: bool - disable_steamdeck_mode: bool - mangohud_workaround: bool - disable_vkbasalt: bool +# Import auto-generated configuration components +from .config_schema_generated import ConfigurationData, get_script_parsing_logic, get_script_generation_logic + +# Note: ConfigurationData is now imported from generated file +# No need to manually maintain the TypedDict anymore! class ConfigurationManager: @@ -295,46 +290,9 @@ class ConfigurationManager: Returns: Dict containing parsed script-only field values """ - script_values = {} - - try: - lines = script_content.split('\n') - - for line in lines: - line = line.strip() - - # Skip comments, empty lines, and non-export lines - if not line or line.startswith('#') or not line.startswith('export '): - continue - - # Parse export statements: export VAR=value - if '=' in line: - # Remove 'export ' prefix - export_line = line[len('export '):] - key, value = export_line.split('=', 1) - key = key.strip() - value = value.strip() - - # Map environment variables to config field names - if key == "DXVK_FRAME_RATE": - try: - script_values["dxvk_frame_rate"] = int(value) - except ValueError: - pass - elif key == "PROTON_USE_WOW64": - script_values["enable_wow64"] = value == "1" - elif key == "SteamDeck": - script_values["disable_steamdeck_mode"] = value == "0" - elif key == "MANGOHUD": - script_values["mangohud_workaround"] = value == "1" - elif key == "DISABLE_VKBASALT": - script_values["disable_vkbasalt"] = value == "1" - - except (ValueError, KeyError, IndexError) as e: - # If parsing fails, log the error and return empty dict (will use defaults) - print(f"Error parsing script content: {e}") - - return script_values + # Use auto-generated parsing logic + parse_script_values = get_script_parsing_logic() + return parse_script_values(script_content.split('\n')) @staticmethod def merge_config_with_script(toml_config: ConfigurationData, script_values: Dict[str, Union[bool, int, str]]) -> ConfigurationData: diff --git a/py_modules/lsfg_vk/config_schema_generated.py b/py_modules/lsfg_vk/config_schema_generated.py new file mode 100644 index 0000000..46bc58f --- /dev/null +++ b/py_modules/lsfg_vk/config_schema_generated.py @@ -0,0 +1,118 @@ +""" +Auto-generated configuration schema components from shared_config.py +DO NOT EDIT THIS FILE MANUALLY - it will be overwritten on build +""" + +from typing import TypedDict, Dict, Any, Union, cast +from enum import Enum +import sys +from pathlib import Path + +# Import shared configuration constants +sys.path.insert(0, str(Path(__file__).parent.parent.parent)) +from shared_config import CONFIG_SCHEMA_DEF, ConfigFieldType + + +class ConfigurationData(TypedDict): + """Type-safe configuration data structure - AUTO-GENERATED""" + dll: str + multiplier: int + flow_scale: float + performance_mode: bool + hdr_mode: bool + experimental_present_mode: str + dxvk_frame_rate: int + enable_wow64: bool + disable_steamdeck_mode: bool + mangohud_workaround: bool + disable_vkbasalt: bool + + +def get_script_parsing_logic(): + """Return the script parsing logic as a callable""" + def parse_script_values(lines): + script_values = {} + for line in lines: + line = line.strip() + if not line or line.startswith("#") or not line.startswith("export "): + continue + if "=" in line: + export_line = line[len("export "):] + key, value = export_line.split("=", 1) + key = key.strip() + value = value.strip() + + # Auto-generated parsing logic: + if key == "DXVK_FRAME_RATE": + try: + script_values["dxvk_frame_rate"] = int(value) + except ValueError: + pass + if key == "PROTON_USE_WOW64": + script_values["enable_wow64"] = value == "1" + if key == "SteamDeck": + script_values["disable_steamdeck_mode"] = value == "0" + if key == "MANGOHUD": + script_values["mangohud_workaround"] = value == "1" + if key == "DISABLE_VKBASALT": + script_values["disable_vkbasalt"] = value == "1" + + return script_values + return parse_script_values + + +def get_script_generation_logic(): + """Return the script generation logic as a callable""" + def generate_script_lines(config): + lines = [] + dxvk_frame_rate = config.get("dxvk_frame_rate", 0) + if dxvk_frame_rate > 0: + lines.append(f"export DXVK_FRAME_RATE={dxvk_frame_rate}") + if config.get("enable_wow64", False): + lines.append("export PROTON_USE_WOW64=1") + if config.get("disable_steamdeck_mode", False): + lines.append("export SteamDeck=0") + if config.get("mangohud_workaround", False): + lines.append("export MANGOHUD=1") + if config.get("disable_vkbasalt", False): + lines.append("export DISABLE_VKBASALT=1") + return lines + return generate_script_lines + + +def get_function_parameters() -> str: + """Return function signature parameters""" + return """dll: str = "/games/Lossless Scaling/Lossless.dll", + multiplier: int = 1, + flow_scale: float = 0.8, + performance_mode: bool = True, + hdr_mode: bool = False, + experimental_present_mode: str = "fifo", + dxvk_frame_rate: int = 0, + enable_wow64: bool = False, + disable_steamdeck_mode: bool = False, + mangohud_workaround: bool = False, + disable_vkbasalt: bool = False""" + + +def create_config_dict(**kwargs) -> ConfigurationData: + """Create configuration dictionary from keyword arguments""" + return cast(ConfigurationData, { + "dll": dll, + "multiplier": multiplier, + "flow_scale": flow_scale, + "performance_mode": performance_mode, + "hdr_mode": hdr_mode, + "experimental_present_mode": experimental_present_mode, + "dxvk_frame_rate": dxvk_frame_rate, + "enable_wow64": enable_wow64, + "disable_steamdeck_mode": disable_steamdeck_mode, + "mangohud_workaround": mangohud_workaround, + "disable_vkbasalt": disable_vkbasalt, + }) + + +# Field lists for dynamic operations +TOML_FIELDS = ['dll', 'multiplier', 'flow_scale', 'performance_mode', 'hdr_mode', 'experimental_present_mode'] +SCRIPT_FIELDS = ['dxvk_frame_rate', 'enable_wow64', 'disable_steamdeck_mode', 'mangohud_workaround', 'disable_vkbasalt'] +ALL_FIELDS = ['dll', 'multiplier', 'flow_scale', 'performance_mode', 'hdr_mode', 'experimental_present_mode', 'dxvk_frame_rate', 'enable_wow64', 'disable_steamdeck_mode', 'mangohud_workaround', 'disable_vkbasalt'] diff --git a/py_modules/lsfg_vk/configuration.py b/py_modules/lsfg_vk/configuration.py index 49653ea..e745e29 100644 --- a/py_modules/lsfg_vk/configuration.py +++ b/py_modules/lsfg_vk/configuration.py @@ -6,7 +6,9 @@ from pathlib import Path from typing import Dict, Any from .base_service import BaseService -from .config_schema import ConfigurationManager, ConfigurationData, CONFIG_SCHEMA +from .config_schema import ConfigurationManager, CONFIG_SCHEMA +from .config_schema_generated import ConfigurationData, get_script_generation_logic +from .configuration_helpers_generated import log_configuration_update from .types import ConfigurationResponse @@ -149,13 +151,8 @@ class ConfigurationService(BaseService): if not script_result["success"]: self.log.warning(f"Failed to update launch script: {script_result['error']}") - self.log.info(f"Updated lsfg TOML configuration: " - f"dll='{dll}', multiplier={multiplier}, flow_scale={flow_scale}, " - f"performance_mode={performance_mode}, hdr_mode={hdr_mode}, " - f"experimental_present_mode='{experimental_present_mode}', " - f"dxvk_frame_rate={dxvk_frame_rate}, " - f"enable_wow64={enable_wow64}, disable_steamdeck_mode={disable_steamdeck_mode}, " - f"mangohud_workaround={mangohud_workaround}, disable_vkbasalt={disable_vkbasalt}") + # Use auto-generated logging + log_configuration_update(self.log, config) return self._success_response(ConfigurationResponse, "lsfg configuration updated successfully", @@ -254,29 +251,14 @@ class ConfigurationService(BaseService): "# This script sets up the environment for lsfg-vk to work with the plugin configuration" ] - # Add optional export statements based on configuration - if config.get("enable_wow64", False): - lines.append("export PROTON_USE_WOW64=1") + # Use auto-generated script generation logic + generate_script_lines = get_script_generation_logic() + lines.extend(generate_script_lines(config)) - if config.get("disable_steamdeck_mode", False): - lines.append("export SteamDeck=0") - - if config.get("mangohud_workaround", False): - lines.append("export MANGOHUD=1") - lines.append("export MANGOHUD_CONFIG=alpha=0.001,background_alpha=0.001") - - if config.get("disable_vkbasalt", False): - lines.append("export DISABLE_VKBASALT=1") - - # Add DXVK_FRAME_RATE if dxvk_frame_rate is set - dxvk_frame_rate = config.get("dxvk_frame_rate", 0) - if dxvk_frame_rate > 0: - lines.append(f"export DXVK_FRAME_RATE={dxvk_frame_rate}") - - # Always add the LSFG_PROCESS export - lines.append("export LSFG_PROCESS=decky-lsfg-vk") - - # Add the execution line - lines.append('exec "$@"') + # Always add the LSFG_PROCESS export and execution line + lines.extend([ + "export LSFG_PROCESS=decky-lsfg-vk", + 'exec "$@"' + ]) return "\n".join(lines) + "\n" diff --git a/py_modules/lsfg_vk/configuration_helpers_generated.py b/py_modules/lsfg_vk/configuration_helpers_generated.py new file mode 100644 index 0000000..f9f4a65 --- /dev/null +++ b/py_modules/lsfg_vk/configuration_helpers_generated.py @@ -0,0 +1,22 @@ +""" +Auto-generated configuration helper functions from shared_config.py +DO NOT EDIT THIS FILE MANUALLY - it will be overwritten on build +""" + +from typing import Dict, Any +from .config_schema_generated import ConfigurationData, ALL_FIELDS + + +def log_configuration_update(logger, config: ConfigurationData) -> None: + """Log configuration update with all field values""" + logger.info(f"Updated lsfg TOML configuration: dll={config['dll']}, multiplier={config['multiplier']}, flow_scale={config['flow_scale']}, performance_mode={config['performance_mode']}, hdr_mode={config['hdr_mode']}, experimental_present_mode={config['experimental_present_mode']}, dxvk_frame_rate={config['dxvk_frame_rate']}, enable_wow64={config['enable_wow64']}, disable_steamdeck_mode={config['disable_steamdeck_mode']}, mangohud_workaround={config['mangohud_workaround']}, disable_vkbasalt={config['disable_vkbasalt']}") + + +def get_config_field_names() -> list[str]: + """Get all configuration field names""" + return ALL_FIELDS.copy() + + +def extract_config_values(config: ConfigurationData) -> Dict[str, Any]: + """Extract configuration values as a dictionary""" + return {field: config[field] for field in ALL_FIELDS} diff --git a/scripts/generate_python_boilerplate.py b/scripts/generate_python_boilerplate.py new file mode 100644 index 0000000..0101ae4 --- /dev/null +++ b/scripts/generate_python_boilerplate.py @@ -0,0 +1,319 @@ +#!/usr/bin/env python3 +""" +Generate Python boilerplate from shared_config.py + +This script generates repetitive Python code patterns from the canonical schema, +reducing manual maintenance when adding/removing configuration fields. +""" + +import sys +from pathlib import Path + +# Add project root to path to import shared_config +project_root = Path(__file__).parent.parent +sys.path.insert(0, str(project_root)) + +from shared_config import CONFIG_SCHEMA_DEF, ConfigFieldType + + +def get_python_type(field_type: ConfigFieldType) -> str: + """Convert ConfigFieldType to Python type annotation""" + type_map = { + ConfigFieldType.BOOLEAN: "bool", + ConfigFieldType.INTEGER: "int", + ConfigFieldType.FLOAT: "float", + ConfigFieldType.STRING: "str" + } + return type_map.get(field_type, "Any") + + +def get_env_var_name(field_name: str) -> str: + """Convert field name to environment variable name""" + env_map = { + "dxvk_frame_rate": "DXVK_FRAME_RATE", + "enable_wow64": "PROTON_USE_WOW64", + "disable_steamdeck_mode": "SteamDeck", + "mangohud_workaround": "MANGOHUD", + "disable_vkbasalt": "DISABLE_VKBASALT" + } + return env_map.get(field_name, field_name.upper()) + + +def generate_typed_dict() -> str: + """Generate ConfigurationData TypedDict""" + lines = [ + "class ConfigurationData(TypedDict):", + " \"\"\"Type-safe configuration data structure - AUTO-GENERATED\"\"\"" + ] + + for field_name, field_def in CONFIG_SCHEMA_DEF.items(): + python_type = get_python_type(ConfigFieldType(field_def["fieldType"])) + lines.append(f" {field_name}: {python_type}") + + return "\n".join(lines) + + +def generate_function_signature() -> str: + """Generate function signature for update_config and create_config_from_args""" + params = [] + + for field_name, field_def in CONFIG_SCHEMA_DEF.items(): + python_type = get_python_type(ConfigFieldType(field_def["fieldType"])) + default = field_def["default"] + + # Format default value + if isinstance(default, str): + default_str = f'"{default}"' + elif isinstance(default, bool): + default_str = str(default) + else: + default_str = str(default) + + params.append(f"{field_name}: {python_type} = {default_str}") + + return ",\n ".join(params) + + +def generate_config_dict_creation() -> str: + """Generate dictionary creation for create_config_from_args""" + lines = [" return cast(ConfigurationData, {"] + + for field_name in CONFIG_SCHEMA_DEF.keys(): + lines.append(f' "{field_name}": {field_name},') + + lines.append(" })") + return "\n".join(lines) + + +def generate_script_parsing() -> str: + """Generate script content parsing logic""" + lines = [] + + script_fields = [ + (field_name, field_def) + for field_name, field_def in CONFIG_SCHEMA_DEF.items() + if field_def.get("location") == "script" + ] + + for field_name, field_def in script_fields: + env_var = get_env_var_name(field_name) + field_type = ConfigFieldType(field_def["fieldType"]) + + if field_type == ConfigFieldType.BOOLEAN: + if field_name == "disable_steamdeck_mode": + # Special case: SteamDeck=0 means disable_steamdeck_mode=True + lines.append(f' elif key == "{env_var}":') + lines.append(f' script_values["{field_name}"] = value == "0"') + else: + lines.append(f' elif key == "{env_var}":') + lines.append(f' script_values["{field_name}"] = value == "1"') + elif field_type == ConfigFieldType.INTEGER: + lines.append(f' elif key == "{env_var}":') + lines.append(' try:') + lines.append(f' script_values["{field_name}"] = int(value)') + lines.append(' except ValueError:') + lines.append(' pass') + elif field_type == ConfigFieldType.FLOAT: + lines.append(f' elif key == "{env_var}":') + lines.append(' try:') + lines.append(f' script_values["{field_name}"] = float(value)') + lines.append(' except ValueError:') + lines.append(' pass') + elif field_type == ConfigFieldType.STRING: + lines.append(f' elif key == "{env_var}":') + lines.append(f' script_values["{field_name}"] = value') + + return "\n".join(lines) + + +def generate_script_generation() -> str: + """Generate script content generation logic""" + lines = [] + + script_fields = [ + (field_name, field_def) + for field_name, field_def in CONFIG_SCHEMA_DEF.items() + if field_def.get("location") == "script" + ] + + for field_name, field_def in script_fields: + env_var = get_env_var_name(field_name) + field_type = ConfigFieldType(field_def["fieldType"]) + + if field_type == ConfigFieldType.BOOLEAN: + if field_name == "disable_steamdeck_mode": + # Special case: disable_steamdeck_mode=True should export SteamDeck=0 + lines.append(f' if config.get("{field_name}", False):') + lines.append(f' lines.append("export {env_var}=0")') + else: + lines.append(f' if config.get("{field_name}", False):') + lines.append(f' lines.append("export {env_var}=1")') + elif field_type in [ConfigFieldType.INTEGER, ConfigFieldType.FLOAT]: + default = field_def["default"] + if field_name == "dxvk_frame_rate": + # Special handling for DXVK_FRAME_RATE (only export if > 0) + lines.append(f' {field_name} = config.get("{field_name}", {default})') + lines.append(f' if {field_name} > 0:') + lines.append(f' lines.append(f"export {env_var}={{{field_name}}}")') + else: + lines.append(f' {field_name} = config.get("{field_name}", {default})') + lines.append(f' if {field_name} != {default}:') + lines.append(f' lines.append(f"export {env_var}={{{field_name}}}")') + elif field_type == ConfigFieldType.STRING: + lines.append(f' {field_name} = config.get("{field_name}", "")') + lines.append(f' if {field_name}:') + lines.append(f' lines.append(f"export {env_var}={{{field_name}}}")') + + return "\n".join(lines) + + +def generate_log_statement() -> str: + """Generate logging statement with all field values""" + field_parts = [] + + for field_name in CONFIG_SCHEMA_DEF.keys(): + field_parts.append(f"{field_name}={{{field_name}}}") + + log_format = ", ".join(field_parts) + return f' self.log.info(f"Updated lsfg TOML configuration: {log_format}")' + + +def generate_complete_schema_file() -> str: + """Generate complete config_schema_generated.py file""" + lines = [ + '"""', + 'Auto-generated configuration schema components from shared_config.py', + 'DO NOT EDIT THIS FILE MANUALLY - it will be overwritten on build', + '"""', + '', + 'from typing import TypedDict, Dict, Any, Union, cast', + 'from enum import Enum', + 'import sys', + 'from pathlib import Path', + '', + '# Import shared configuration constants', + 'sys.path.insert(0, str(Path(__file__).parent.parent.parent))', + 'from shared_config import CONFIG_SCHEMA_DEF, ConfigFieldType', + '', + '', + generate_typed_dict(), + '', + '', + 'def get_script_parsing_logic():', + ' """Return the script parsing logic as a callable"""', + ' def parse_script_values(lines):', + ' script_values = {}', + ' for line in lines:', + ' line = line.strip()', + ' if not line or line.startswith("#") or not line.startswith("export "):', + ' continue', + ' if "=" in line:', + ' export_line = line[len("export "):]', + ' key, value = export_line.split("=", 1)', + ' key = key.strip()', + ' value = value.strip()', + '', + ' # Auto-generated parsing logic:', + f'{generate_script_parsing().replace(" elif", " if")}', + '', + ' return script_values', + ' return parse_script_values', + '', + '', + 'def get_script_generation_logic():', + ' """Return the script generation logic as a callable"""', + ' def generate_script_lines(config):', + ' lines = []', + f'{generate_script_generation()}', + ' return lines', + ' return generate_script_lines', + '', + '', + 'def get_function_parameters() -> str:', + ' """Return function signature parameters"""', + f' return """{generate_function_signature()}"""', + '', + '', + 'def create_config_dict(**kwargs) -> ConfigurationData:', + ' """Create configuration dictionary from keyword arguments"""', + f'{generate_config_dict_creation().replace(" return cast(ConfigurationData, {", " return cast(ConfigurationData, {").replace(" })", " })")}', + '', + '', + '# Field lists for dynamic operations', + f'TOML_FIELDS = {[name for name, field in CONFIG_SCHEMA_DEF.items() if field.get("location") == "toml"]}', + f'SCRIPT_FIELDS = {[name for name, field in CONFIG_SCHEMA_DEF.items() if field.get("location") == "script"]}', + f'ALL_FIELDS = {list(CONFIG_SCHEMA_DEF.keys())}', + '' + ] + + return '\n'.join(lines) + + +def generate_complete_configuration_helpers() -> str: + """Generate configuration_helpers_generated.py file""" + + # Generate the log format string using config parameter + log_parts = [] + for field_name in CONFIG_SCHEMA_DEF.keys(): + log_parts.append(f"{field_name}={{config['{field_name}']}}") + log_format = ", ".join(log_parts) + + lines = [ + '"""', + 'Auto-generated configuration helper functions from shared_config.py', + 'DO NOT EDIT THIS FILE MANUALLY - it will be overwritten on build', + '"""', + '', + 'from typing import Dict, Any', + 'from .config_schema_generated import ConfigurationData, ALL_FIELDS', + '', + '', + 'def log_configuration_update(logger, config: ConfigurationData) -> None:', + ' """Log configuration update with all field values"""', + f' logger.info(f"Updated lsfg TOML configuration: {log_format}")', + '', + '', + 'def get_config_field_names() -> list[str]:', + ' """Get all configuration field names"""', + ' return ALL_FIELDS.copy()', + '', + '', + 'def extract_config_values(config: ConfigurationData) -> Dict[str, Any]:', + ' """Extract configuration values as a dictionary"""', + ' return {field: config[field] for field in ALL_FIELDS}', + '' + ] + + return '\n'.join(lines) + + +def main(): + """Generate complete Python configuration files""" + try: + # Create generated files in py_modules/lsfg_vk/ + target_dir = project_root / "py_modules" / "lsfg_vk" + + # Generate the complete schema file + schema_content = generate_complete_schema_file() + schema_file = target_dir / "config_schema_generated.py" + schema_file.write_text(schema_content) + print(f"āœ… Generated {schema_file.relative_to(project_root)}") + + # Generate configuration helpers + helpers_content = generate_complete_configuration_helpers() + helpers_file = target_dir / "configuration_helpers_generated.py" + helpers_file.write_text(helpers_content) + print(f"āœ… Generated {helpers_file.relative_to(project_root)}") + + print(f"\nšŸŽÆ Ready-to-use files generated!") + print(" Import these in your main files:") + print(" - from .config_schema_generated import ConfigurationData, get_script_parsing_logic, etc.") + print(" - from .configuration_helpers_generated import log_configuration_update, etc.") + + except Exception as e: + print(f"āŒ Error generating Python files: {e}") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/scripts/generate_ts_schema.py b/scripts/generate_ts_schema.py index dcdddf2..1997c55 100644 --- a/scripts/generate_ts_schema.py +++ b/scripts/generate_ts_schema.py @@ -114,7 +114,7 @@ def generate_typescript_schema(): def main(): - """Main function to generate TypeScript schema""" + """Main function to generate TypeScript schema and Python boilerplate""" try: # Generate the TypeScript content ts_content = generate_typescript_schema() @@ -126,8 +126,22 @@ def main(): print(f"āœ… Generated {target_file} from shared_config.py") print(f" Fields: {len(CONFIG_SCHEMA_DEF)}") + # Also generate Python boilerplate + print("\nšŸ”„ Generating Python boilerplate...") + from pathlib import Path + import subprocess + + boilerplate_script = project_root / "scripts" / "generate_python_boilerplate.py" + result = subprocess.run([sys.executable, str(boilerplate_script)], + capture_output=True, text=True) + + if result.returncode == 0: + print(result.stdout) + else: + print(f"āš ļø Python boilerplate generation had issues:\n{result.stderr}") + except Exception as e: - print(f"āŒ Error generating TypeScript schema: {e}") + print(f"āŒ Error generating schema: {e}") sys.exit(1) diff --git a/src/config/generatedConfigSchema.ts b/src/config/generatedConfigSchema.ts index b7487bd..cb08252 100644 --- a/src/config/generatedConfigSchema.ts +++ b/src/config/generatedConfigSchema.ts @@ -83,18 +83,6 @@ export const CONFIG_SCHEMA: Record = { default: false, description: "Disables vkBasalt layer which can conflict with LSFG (Reshade, some Decky plugins)" }, - foobar_toggle: { - name: "foobar_toggle", - fieldType: ConfigFieldType.BOOLEAN, - default: false, - description: "Test script-only toggle that exports FOOBAR=1 (for testing purposes)" - }, - test_config_only: { - name: "test_config_only", - fieldType: ConfigFieldType.STRING, - default: "default_value", - description: "Test TOML-only configuration field (not in script)" - }, }; // Type-safe configuration data structure @@ -110,8 +98,6 @@ export interface ConfigurationData { disable_steamdeck_mode: boolean; mangohud_workaround: boolean; disable_vkbasalt: boolean; - foobar_toggle: boolean; - test_config_only: string; } // Helper functions @@ -132,8 +118,6 @@ export function getDefaults(): ConfigurationData { disable_steamdeck_mode: false, mangohud_workaround: false, disable_vkbasalt: false, - foobar_toggle: false, - test_config_only: "default_value", }; } @@ -150,8 +134,6 @@ export function getFieldTypes(): Record { disable_steamdeck_mode: ConfigFieldType.BOOLEAN, mangohud_workaround: ConfigFieldType.BOOLEAN, disable_vkbasalt: ConfigFieldType.BOOLEAN, - foobar_toggle: ConfigFieldType.BOOLEAN, - test_config_only: ConfigFieldType.STRING, }; } -- cgit v1.2.3 From d063284dea10e82a23c2c332ecd4901d7254171b Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Tue, 22 Jul 2025 13:23:25 -0400 Subject: use generated kwargs and config in more hardcoded places --- .gitignore | 5 + GENERATED_CODE_ANALYSIS.md | 145 ++++++++++++++++++++++++++ py_modules/lsfg_vk/config_schema.py | 36 ++----- py_modules/lsfg_vk/config_schema_generated.py | 35 +++++-- py_modules/lsfg_vk/configuration.py | 36 ++----- py_modules/lsfg_vk/installation.py | 7 +- scripts/generate_python_boilerplate.py | 16 ++- scripts/generate_ts_schema.py | 9 ++ src/components/ConfigurationSection.tsx | 23 ++-- src/config/configSchema.ts | 6 +- src/config/generatedConfigSchema.ts | 13 +++ 11 files changed, 251 insertions(+), 80 deletions(-) create mode 100644 GENERATED_CODE_ANALYSIS.md diff --git a/.gitignore b/.gitignore index d384256..0297e2f 100644 --- a/.gitignore +++ b/.gitignore @@ -55,3 +55,8 @@ out/* cli/ cli/* cli/decky + +# generated files +py_modules/lsfg_vk/config_schema_generated.py +py_modules/lsfg_vk/configuration_helpers_generated.py +src/config/generatedConfigSchema.ts diff --git a/GENERATED_CODE_ANALYSIS.md b/GENERATED_CODE_ANALYSIS.md new file mode 100644 index 0000000..86c37e6 --- /dev/null +++ b/GENERATED_CODE_ANALYSIS.md @@ -0,0 +1,145 @@ +# Generated Code Analysis and Improvement Opportunities + +## Generated Files Added to .gitignore āœ… + +The following auto-generated files are now properly ignored: +- `py_modules/lsfg_vk/config_schema_generated.py` +- `py_modules/lsfg_vk/configuration_helpers_generated.py` +- `src/config/generatedConfigSchema.ts` + +## Build Scripts Overview + +### 1. `scripts/generate_python_boilerplate.py` +**Generates:** +- `py_modules/lsfg_vk/config_schema_generated.py` - TypedDict, parsing logic, generation logic +- `py_modules/lsfg_vk/configuration_helpers_generated.py` - Helper functions for logging, field access + +### 2. `scripts/generate_ts_schema.py` +**Generates:** +- `src/config/generatedConfigSchema.ts` - TypeScript interfaces, schema constants, defaults + +## Current Hardcoded Areas That Could Use Generated Code + +### Python Side Opportunities + +#### 1. **CRITICAL - Function Signatures** 🚨 +**File:** `py_modules/lsfg_vk/configuration.py:107-113` +```python +def update_config(self, dll: str, multiplier: int, flow_scale: float, + performance_mode: bool, hdr_mode: bool, + experimental_present_mode: str = "fifo", + dxvk_frame_rate: int = 0, + enable_wow64: bool = False, + disable_steamdeck_mode: bool = False, + mangohud_workaround: bool = False, + disable_vkbasalt: bool = False) -> ConfigurationResponse: +``` + +**Improvement:** The generated code already creates `get_function_parameters()` that returns the exact signature string. This method signature should be replaced with a generated version or simplified to use `**kwargs` with schema validation. + +#### 2. **Field Name String Literals** āš ļø +**Multiple locations** - Field names used as string literals: +- `config["dll"]` (multiple files) +- `config["multiplier"]` etc. + +**Improvement:** Use the generated field constants or create typed access methods. + +#### 3. **Configuration Defaults** āš ļø +**File:** Various locations where defaults are hardcoded +**Improvement:** Always use `ConfigurationManager.get_defaults()` or generated default functions. + +#### 4. **Logging Format Strings** āœ… (Partially Done) +**Status:** The `log_configuration_update()` function is already generated and used in some places, but could be used more consistently. + +### TypeScript Side Opportunities + +#### 1. **CRITICAL - Hardcoded Field Names in UI** 🚨 +**File:** `src/components/ConfigurationSection.tsx:48,60,69,78,104,146,164,173,182` + +```tsx +onChange={(value) => onConfigChange('multiplier', value)} +onChange={(value) => onConfigChange('flow_scale', value)} +onChange={(value) => onConfigChange('performance_mode', value)} +// ... etc for all 11 fields +``` + +**Improvement:** These string literals should use schema constants: +```tsx +import { getFieldNames } from '../config/generatedConfigSchema'; +// or use constants like +onChange={(value) => onConfigChange('multiplier', value)} // fieldNames.multiplier +``` + +#### 2. **UI Component Generation** šŸš€ +**Opportunity:** The entire `ConfigurationSection.tsx` could be generated from the schema definition since each field has: +- `fieldType` (determines UI component type) +- `description` (for labels/descriptions) +- `default` (for initial values) +- `name` (for display names) + +#### 3. **Manual Type Definitions** āœ… (Already Done) +**Status:** TypeScript types are already generated properly via `ConfigurationData` interface. + +#### 4. **Field Validation** šŸš€ +**Opportunity:** Client-side validation could be generated from the schema field types and constraints. + +## High-Impact Improvements Recommended + +### Phase 1 - Critical Fixes (High Impact, Low Effort) + +1. **Replace hardcoded field name strings** with schema constants +2. **Use generated default functions** everywhere instead of hardcoded defaults +3. **Consistent use of generated logging functions** + +### Phase 2 - Major Refactoring (High Impact, Medium Effort) + +1. **Replace `update_config()` parameter list** with generated signature or schema-based validation +2. **Generate UI components** from schema instead of manual creation +3. **Create typed field accessor methods** to eliminate string-based field access + +### Phase 3 - Advanced Features (Medium Impact, High Effort) + +1. **Generate client-side validation** from schema constraints +2. **Auto-generate test cases** from schema definition +3. **Generate documentation** from schema descriptions + +## Specific Next Steps + +### Immediate (Phase 1) +1. Create field name constants in TypeScript and use them in ConfigurationSection +2. Replace remaining hardcoded defaults with generated functions +3. Add schema validation to Python configuration updates + +### Short Term (Phase 2) +1. Refactor Python `update_config()` method to use schema-driven approach +2. Generate UI field components from schema metadata +3. Create typed field accessor pattern + +### Long Term (Phase 3) +1. Build complete schema-driven UI generation system +2. Add schema versioning and migration support +3. Generate comprehensive test suites from schema + +## Files That Should Be Modified + +### Python Files +- `py_modules/lsfg_vk/configuration.py` - Method signatures, field access +- `py_modules/lsfg_vk/plugin.py` - Field access patterns +- `py_modules/lsfg_vk/config_schema.py` - Ensure all access uses generated code + +### TypeScript Files +- `src/components/ConfigurationSection.tsx` - Replace hardcoded field names +- `src/config/configSchema.ts` - Add field name constants export +- `src/api/lsfgApi.ts` - Ensure type safety with generated types + +### Build Process +- Consider adding the generation scripts to VS Code tasks or package.json scripts +- Add validation that generated files are up to date in CI/CD + +## Benefits of These Improvements + +1. **Single Source of Truth** - All configuration changes happen in `shared_config.py` +2. **Type Safety** - Reduce runtime errors from typos in field names +3. **Maintainability** - Adding new config fields requires minimal manual code changes +4. **Consistency** - Generated code ensures consistent patterns across Python/TypeScript +5. **Documentation** - Schema serves as living documentation of configuration options diff --git a/py_modules/lsfg_vk/config_schema.py b/py_modules/lsfg_vk/config_schema.py index a7827ae..bbace42 100644 --- a/py_modules/lsfg_vk/config_schema.py +++ b/py_modules/lsfg_vk/config_schema.py @@ -171,7 +171,9 @@ class ConfigurationManager: if config.get("dll"): lines.append("[global]") lines.append(f"# specify where Lossless.dll is stored") - lines.append(f'dll = "{config["dll"]}"') + # Generate TOML lines for TOML fields only - USE GENERATED CONSTANTS + from .config_schema_generated import DLL + lines.append(f'dll = "{config[DLL]}"') lines.append("") # Add game section with process name for LSFG_PROCESS approach @@ -248,9 +250,10 @@ class ConfigurationManager: elif value.startswith("'") and value.endswith("'"): value = value[1:-1] - # Handle global section (dll only) + # Handle global section (dll only) - USE GENERATED CONSTANTS if in_global_section and key == "dll": - config["dll"] = value + from .config_schema_generated import DLL + config[DLL] = value # Handle game section elif in_game_section: @@ -315,25 +318,8 @@ class ConfigurationManager: return cast(ConfigurationData, merged_config) @staticmethod - def create_config_from_args(dll: str, multiplier: int, flow_scale: float, - performance_mode: bool, hdr_mode: bool, - experimental_present_mode: str = "fifo", - dxvk_frame_rate: int = 0, - enable_wow64: bool = False, - disable_steamdeck_mode: bool = False, - mangohud_workaround: bool = False, - disable_vkbasalt: bool = False) -> ConfigurationData: - """Create configuration from individual arguments""" - return cast(ConfigurationData, { - "dll": dll, - "multiplier": multiplier, - "flow_scale": flow_scale, - "performance_mode": performance_mode, - "hdr_mode": hdr_mode, - "experimental_present_mode": experimental_present_mode, - "dxvk_frame_rate": dxvk_frame_rate, - "enable_wow64": enable_wow64, - "disable_steamdeck_mode": disable_steamdeck_mode, - "mangohud_workaround": mangohud_workaround, - "disable_vkbasalt": disable_vkbasalt - }) + @staticmethod + def create_config_from_args(**kwargs) -> ConfigurationData: + """Create configuration from keyword arguments - USES GENERATED CODE""" + from .config_schema_generated import create_config_dict + return create_config_dict(**kwargs) diff --git a/py_modules/lsfg_vk/config_schema_generated.py b/py_modules/lsfg_vk/config_schema_generated.py index 46bc58f..cc90207 100644 --- a/py_modules/lsfg_vk/config_schema_generated.py +++ b/py_modules/lsfg_vk/config_schema_generated.py @@ -12,6 +12,19 @@ from pathlib import Path sys.path.insert(0, str(Path(__file__).parent.parent.parent)) from shared_config import CONFIG_SCHEMA_DEF, ConfigFieldType +# Field name constants for type-safe access +DLL = "dll" +MULTIPLIER = "multiplier" +FLOW_SCALE = "flow_scale" +PERFORMANCE_MODE = "performance_mode" +HDR_MODE = "hdr_mode" +EXPERIMENTAL_PRESENT_MODE = "experimental_present_mode" +DXVK_FRAME_RATE = "dxvk_frame_rate" +ENABLE_WOW64 = "enable_wow64" +DISABLE_STEAMDECK_MODE = "disable_steamdeck_mode" +MANGOHUD_WORKAROUND = "mangohud_workaround" +DISABLE_VKBASALT = "disable_vkbasalt" + class ConfigurationData(TypedDict): """Type-safe configuration data structure - AUTO-GENERATED""" @@ -98,17 +111,17 @@ def get_function_parameters() -> str: def create_config_dict(**kwargs) -> ConfigurationData: """Create configuration dictionary from keyword arguments""" return cast(ConfigurationData, { - "dll": dll, - "multiplier": multiplier, - "flow_scale": flow_scale, - "performance_mode": performance_mode, - "hdr_mode": hdr_mode, - "experimental_present_mode": experimental_present_mode, - "dxvk_frame_rate": dxvk_frame_rate, - "enable_wow64": enable_wow64, - "disable_steamdeck_mode": disable_steamdeck_mode, - "mangohud_workaround": mangohud_workaround, - "disable_vkbasalt": disable_vkbasalt, + "dll": kwargs.get("dll"), + "multiplier": kwargs.get("multiplier"), + "flow_scale": kwargs.get("flow_scale"), + "performance_mode": kwargs.get("performance_mode"), + "hdr_mode": kwargs.get("hdr_mode"), + "experimental_present_mode": kwargs.get("experimental_present_mode"), + "dxvk_frame_rate": kwargs.get("dxvk_frame_rate"), + "enable_wow64": kwargs.get("enable_wow64"), + "disable_steamdeck_mode": kwargs.get("disable_steamdeck_mode"), + "mangohud_workaround": kwargs.get("mangohud_workaround"), + "disable_vkbasalt": kwargs.get("disable_vkbasalt"), }) diff --git a/py_modules/lsfg_vk/configuration.py b/py_modules/lsfg_vk/configuration.py index e745e29..b9ee174 100644 --- a/py_modules/lsfg_vk/configuration.py +++ b/py_modules/lsfg_vk/configuration.py @@ -103,39 +103,18 @@ class ConfigurationService(BaseService): self.log.error(error_msg) return self._error_response(ConfigurationResponse, str(e), config=None) - def update_config(self, dll: str, multiplier: int, flow_scale: float, - performance_mode: bool, hdr_mode: bool, - experimental_present_mode: str = "fifo", - dxvk_frame_rate: int = 0, - enable_wow64: bool = False, - disable_steamdeck_mode: bool = False, - mangohud_workaround: bool = False, - disable_vkbasalt: bool = False) -> ConfigurationResponse: - """Update TOML configuration + def update_config(self, **kwargs) -> ConfigurationResponse: + """Update TOML configuration using generated schema - SIMPLIFIED WITH GENERATED CODE Args: - dll: Path to Lossless.dll - multiplier: LSFG multiplier value - flow_scale: LSFG flow scale value - performance_mode: Whether to enable performance mode - hdr_mode: Whether to enable HDR mode - experimental_present_mode: Experimental Vulkan present mode override - dxvk_frame_rate: Frame rate cap for DirectX games, before frame multiplier (0 = disabled) - enable_wow64: Whether to enable PROTON_USE_WOW64=1 for 32-bit games - disable_steamdeck_mode: Whether to disable Steam Deck mode - mangohud_workaround: Whether to enable MangoHud workaround with transparent overlay - disable_vkbasalt: Whether to disable vkBasalt layer + **kwargs: Configuration field values (see shared_config.py for available fields) Returns: ConfigurationResponse with success status """ try: - # Create configuration from individual arguments - config = ConfigurationManager.create_config_from_args( - dll, multiplier, flow_scale, performance_mode, hdr_mode, - experimental_present_mode, dxvk_frame_rate, enable_wow64, disable_steamdeck_mode, - mangohud_workaround, disable_vkbasalt - ) + # Create configuration from keyword arguments using generated function + config = ConfigurationManager.create_config_from_args(**kwargs) # Generate TOML content using centralized manager toml_content = ConfigurationManager.generate_toml_content(config) @@ -187,8 +166,9 @@ class ConfigurationService(BaseService): else: config = current_response["config"] - # Update just the DLL path - config["dll"] = dll_path + # Update just the DLL path - USE GENERATED CONSTANTS + from .config_schema_generated import DLL + config[DLL] = dll_path # Generate TOML content and write it toml_content = ConfigurationManager.generate_toml_content(config) diff --git a/py_modules/lsfg_vk/installation.py b/py_modules/lsfg_vk/installation.py index b340093..996a03f 100644 --- a/py_modules/lsfg_vk/installation.py +++ b/py_modules/lsfg_vk/installation.py @@ -121,9 +121,10 @@ class InstallationService(BaseService): self._write_file(self.config_file_path, toml_content, 0o644) self.log.info(f"Created config file at {self.config_file_path}") - # Log detected DLL path if found - if config["dll"]: - self.log.info(f"Configured DLL path: {config['dll']}") + # Log detected DLL path if found - USE GENERATED CONSTANTS + from .config_schema_generated import DLL + if config[DLL]: + self.log.info(f"Configured DLL path: {config[DLL]}") def _create_lsfg_launch_script(self) -> None: """Create the ~/lsfg launch script for easier game setup""" diff --git a/scripts/generate_python_boilerplate.py b/scripts/generate_python_boilerplate.py index 0101ae4..b16aa3f 100644 --- a/scripts/generate_python_boilerplate.py +++ b/scripts/generate_python_boilerplate.py @@ -76,12 +76,12 @@ def generate_function_signature() -> str: def generate_config_dict_creation() -> str: """Generate dictionary creation for create_config_from_args""" - lines = [" return cast(ConfigurationData, {"] + lines = [" return cast(ConfigurationData, {"] for field_name in CONFIG_SCHEMA_DEF.keys(): - lines.append(f' "{field_name}": {field_name},') + lines.append(f' "{field_name}": kwargs.get("{field_name}"),') - lines.append(" })") + lines.append(" })") return "\n".join(lines) @@ -180,6 +180,13 @@ def generate_log_statement() -> str: def generate_complete_schema_file() -> str: """Generate complete config_schema_generated.py file""" + + # Generate field name constants + field_constants = [] + for field_name in CONFIG_SCHEMA_DEF.keys(): + const_name = field_name.upper() + field_constants.append(f'{const_name} = "{field_name}"') + lines = [ '"""', 'Auto-generated configuration schema components from shared_config.py', @@ -194,6 +201,9 @@ def generate_complete_schema_file() -> str: '# Import shared configuration constants', 'sys.path.insert(0, str(Path(__file__).parent.parent.parent))', 'from shared_config import CONFIG_SCHEMA_DEF, ConfigFieldType', + '', + '# Field name constants for type-safe access', + ] + field_constants + [ '', '', generate_typed_dict(), diff --git a/scripts/generate_ts_schema.py b/scripts/generate_ts_schema.py index 1997c55..c4c0e8a 100644 --- a/scripts/generate_ts_schema.py +++ b/scripts/generate_ts_schema.py @@ -19,6 +19,12 @@ from shared_config import CONFIG_SCHEMA_DEF, ConfigFieldType def generate_typescript_schema(): """Generate generatedConfigSchema.ts from Python schema""" + # Generate field name constants + field_constants = [] + for field_name in CONFIG_SCHEMA_DEF.keys(): + const_name = field_name.upper() + field_constants.append(f'export const {const_name} = "{field_name}" as const;') + # Generate enum enum_lines = [ "// src/config/generatedConfigSchema.ts", @@ -29,6 +35,9 @@ def generate_typescript_schema(): " FLOAT = \"float\",", " STRING = \"string\"", "}", + "", + "// Field name constants for type-safe access", + ] + field_constants + [ "", "// Configuration field definition", "export interface ConfigField {", diff --git a/src/components/ConfigurationSection.tsx b/src/components/ConfigurationSection.tsx index 1c0d2b2..c0b67fd 100644 --- a/src/components/ConfigurationSection.tsx +++ b/src/components/ConfigurationSection.tsx @@ -1,5 +1,10 @@ import { PanelSectionRow, ToggleField, SliderField, DropdownItem } from "@decky/ui"; import { ConfigurationData } from "../config/configSchema"; +import { + MULTIPLIER, FLOW_SCALE, PERFORMANCE_MODE, HDR_MODE, + EXPERIMENTAL_PRESENT_MODE, DXVK_FRAME_RATE, DISABLE_STEAMDECK_MODE, + MANGOHUD_WORKAROUND, DISABLE_VKBASALT +} from "../config/generatedConfigSchema"; interface ConfigurationSectionProps { config: ConfigurationData; @@ -45,7 +50,7 @@ export function ConfigurationSection({ ]} showValue={false} notchTicksVisible={true} - onChange={(value) => onConfigChange('multiplier', value)} + onChange={(value) => onConfigChange(MULTIPLIER, value)} /> @@ -57,7 +62,7 @@ export function ConfigurationSection({ min={0.25} max={1.0} step={0.01} - onChange={(value) => onConfigChange('flow_scale', value)} + onChange={(value) => onConfigChange(FLOW_SCALE, value)} /> @@ -66,7 +71,7 @@ export function ConfigurationSection({ label="Performance Mode" description="Uses a lighter model for FG (Recommended for most games)" checked={config.performance_mode} - onChange={(value) => onConfigChange('performance_mode', value)} + onChange={(value) => onConfigChange(PERFORMANCE_MODE, value)} /> @@ -75,7 +80,7 @@ export function ConfigurationSection({ label="HDR Mode" description="Enables HDR mode (only for games that support HDR)" checked={config.hdr_mode} - onChange={(value) => onConfigChange('hdr_mode', value)} + onChange={(value) => onConfigChange(HDR_MODE, value)} /> @@ -101,7 +106,7 @@ export function ConfigurationSection({ description="Select a specific Vulkan presentation mode for better performance or compatibility (May cause crashes)" menuLabel="Select presentation mode" selectedOption={config.experimental_present_mode || "fifo"} - onChange={(value) => onConfigChange('experimental_present_mode', value.data)} + onChange={(value) => onConfigChange(EXPERIMENTAL_PRESENT_MODE, value.data)} rgOptions={[ { data: "fifo", label: "FIFO (VSync) - Default" }, { data: "mailbox", label: "Mailbox" } @@ -143,7 +148,7 @@ export function ConfigurationSection({ min={0} max={60} step={1} - onChange={(value) => onConfigChange('dxvk_frame_rate', value)} + onChange={(value) => onConfigChange(DXVK_FRAME_RATE, value)} /> @@ -161,7 +166,7 @@ export function ConfigurationSection({ label="Disable Steam Deck Mode" description="Disables Steam Deck mode (Unlocks hidden settings in some games)" checked={config.disable_steamdeck_mode} - onChange={(value) => onConfigChange('disable_steamdeck_mode', value)} + onChange={(value) => onConfigChange(DISABLE_STEAMDECK_MODE, value)} /> @@ -170,7 +175,7 @@ export function ConfigurationSection({ label="MangoHud Workaround" description="Enables a transparent mangohud overlay, sometimes fixes issues with 2X multiplier in game mode" checked={config.mangohud_workaround} - onChange={(value) => onConfigChange('mangohud_workaround', value)} + onChange={(value) => onConfigChange(MANGOHUD_WORKAROUND, value)} /> @@ -179,7 +184,7 @@ export function ConfigurationSection({ label="Disable vkBasalt" description="Disables vkBasalt layer which can conflict with LSFG (Reshade, some Decky plugins)" checked={config.disable_vkbasalt} - onChange={(value) => onConfigChange('disable_vkbasalt', value)} + onChange={(value) => onConfigChange(DISABLE_VKBASALT, value)} /> diff --git a/src/config/configSchema.ts b/src/config/configSchema.ts index fdf6212..9568fd8 100644 --- a/src/config/configSchema.ts +++ b/src/config/configSchema.ts @@ -18,7 +18,11 @@ export { ConfigurationData, getFieldNames, getDefaults, - getFieldTypes + getFieldTypes, + // Field name constants for type-safe access + DLL, MULTIPLIER, FLOW_SCALE, PERFORMANCE_MODE, HDR_MODE, + EXPERIMENTAL_PRESENT_MODE, DXVK_FRAME_RATE, ENABLE_WOW64, + DISABLE_STEAMDECK_MODE, MANGOHUD_WORKAROUND, DISABLE_VKBASALT } from './generatedConfigSchema'; /** diff --git a/src/config/generatedConfigSchema.ts b/src/config/generatedConfigSchema.ts index cb08252..4a301a1 100644 --- a/src/config/generatedConfigSchema.ts +++ b/src/config/generatedConfigSchema.ts @@ -7,6 +7,19 @@ export enum ConfigFieldType { STRING = "string" } +// Field name constants for type-safe access +export const DLL = "dll" as const; +export const MULTIPLIER = "multiplier" as const; +export const FLOW_SCALE = "flow_scale" as const; +export const PERFORMANCE_MODE = "performance_mode" as const; +export const HDR_MODE = "hdr_mode" as const; +export const EXPERIMENTAL_PRESENT_MODE = "experimental_present_mode" as const; +export const DXVK_FRAME_RATE = "dxvk_frame_rate" as const; +export const ENABLE_WOW64 = "enable_wow64" as const; +export const DISABLE_STEAMDECK_MODE = "disable_steamdeck_mode" as const; +export const MANGOHUD_WORKAROUND = "mangohud_workaround" as const; +export const DISABLE_VKBASALT = "disable_vkbasalt" as const; + // Configuration field definition export interface ConfigField { name: string; -- cgit v1.2.3 From 71f7d0b83454c189ef3ec7199db628b424d1baec Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Tue, 22 Jul 2025 13:23:54 -0400 Subject: rm writeup thing lol --- GENERATED_CODE_ANALYSIS.md | 145 --------------------------------------------- 1 file changed, 145 deletions(-) delete mode 100644 GENERATED_CODE_ANALYSIS.md diff --git a/GENERATED_CODE_ANALYSIS.md b/GENERATED_CODE_ANALYSIS.md deleted file mode 100644 index 86c37e6..0000000 --- a/GENERATED_CODE_ANALYSIS.md +++ /dev/null @@ -1,145 +0,0 @@ -# Generated Code Analysis and Improvement Opportunities - -## Generated Files Added to .gitignore āœ… - -The following auto-generated files are now properly ignored: -- `py_modules/lsfg_vk/config_schema_generated.py` -- `py_modules/lsfg_vk/configuration_helpers_generated.py` -- `src/config/generatedConfigSchema.ts` - -## Build Scripts Overview - -### 1. `scripts/generate_python_boilerplate.py` -**Generates:** -- `py_modules/lsfg_vk/config_schema_generated.py` - TypedDict, parsing logic, generation logic -- `py_modules/lsfg_vk/configuration_helpers_generated.py` - Helper functions for logging, field access - -### 2. `scripts/generate_ts_schema.py` -**Generates:** -- `src/config/generatedConfigSchema.ts` - TypeScript interfaces, schema constants, defaults - -## Current Hardcoded Areas That Could Use Generated Code - -### Python Side Opportunities - -#### 1. **CRITICAL - Function Signatures** 🚨 -**File:** `py_modules/lsfg_vk/configuration.py:107-113` -```python -def update_config(self, dll: str, multiplier: int, flow_scale: float, - performance_mode: bool, hdr_mode: bool, - experimental_present_mode: str = "fifo", - dxvk_frame_rate: int = 0, - enable_wow64: bool = False, - disable_steamdeck_mode: bool = False, - mangohud_workaround: bool = False, - disable_vkbasalt: bool = False) -> ConfigurationResponse: -``` - -**Improvement:** The generated code already creates `get_function_parameters()` that returns the exact signature string. This method signature should be replaced with a generated version or simplified to use `**kwargs` with schema validation. - -#### 2. **Field Name String Literals** āš ļø -**Multiple locations** - Field names used as string literals: -- `config["dll"]` (multiple files) -- `config["multiplier"]` etc. - -**Improvement:** Use the generated field constants or create typed access methods. - -#### 3. **Configuration Defaults** āš ļø -**File:** Various locations where defaults are hardcoded -**Improvement:** Always use `ConfigurationManager.get_defaults()` or generated default functions. - -#### 4. **Logging Format Strings** āœ… (Partially Done) -**Status:** The `log_configuration_update()` function is already generated and used in some places, but could be used more consistently. - -### TypeScript Side Opportunities - -#### 1. **CRITICAL - Hardcoded Field Names in UI** 🚨 -**File:** `src/components/ConfigurationSection.tsx:48,60,69,78,104,146,164,173,182` - -```tsx -onChange={(value) => onConfigChange('multiplier', value)} -onChange={(value) => onConfigChange('flow_scale', value)} -onChange={(value) => onConfigChange('performance_mode', value)} -// ... etc for all 11 fields -``` - -**Improvement:** These string literals should use schema constants: -```tsx -import { getFieldNames } from '../config/generatedConfigSchema'; -// or use constants like -onChange={(value) => onConfigChange('multiplier', value)} // fieldNames.multiplier -``` - -#### 2. **UI Component Generation** šŸš€ -**Opportunity:** The entire `ConfigurationSection.tsx` could be generated from the schema definition since each field has: -- `fieldType` (determines UI component type) -- `description` (for labels/descriptions) -- `default` (for initial values) -- `name` (for display names) - -#### 3. **Manual Type Definitions** āœ… (Already Done) -**Status:** TypeScript types are already generated properly via `ConfigurationData` interface. - -#### 4. **Field Validation** šŸš€ -**Opportunity:** Client-side validation could be generated from the schema field types and constraints. - -## High-Impact Improvements Recommended - -### Phase 1 - Critical Fixes (High Impact, Low Effort) - -1. **Replace hardcoded field name strings** with schema constants -2. **Use generated default functions** everywhere instead of hardcoded defaults -3. **Consistent use of generated logging functions** - -### Phase 2 - Major Refactoring (High Impact, Medium Effort) - -1. **Replace `update_config()` parameter list** with generated signature or schema-based validation -2. **Generate UI components** from schema instead of manual creation -3. **Create typed field accessor methods** to eliminate string-based field access - -### Phase 3 - Advanced Features (Medium Impact, High Effort) - -1. **Generate client-side validation** from schema constraints -2. **Auto-generate test cases** from schema definition -3. **Generate documentation** from schema descriptions - -## Specific Next Steps - -### Immediate (Phase 1) -1. Create field name constants in TypeScript and use them in ConfigurationSection -2. Replace remaining hardcoded defaults with generated functions -3. Add schema validation to Python configuration updates - -### Short Term (Phase 2) -1. Refactor Python `update_config()` method to use schema-driven approach -2. Generate UI field components from schema metadata -3. Create typed field accessor pattern - -### Long Term (Phase 3) -1. Build complete schema-driven UI generation system -2. Add schema versioning and migration support -3. Generate comprehensive test suites from schema - -## Files That Should Be Modified - -### Python Files -- `py_modules/lsfg_vk/configuration.py` - Method signatures, field access -- `py_modules/lsfg_vk/plugin.py` - Field access patterns -- `py_modules/lsfg_vk/config_schema.py` - Ensure all access uses generated code - -### TypeScript Files -- `src/components/ConfigurationSection.tsx` - Replace hardcoded field names -- `src/config/configSchema.ts` - Add field name constants export -- `src/api/lsfgApi.ts` - Ensure type safety with generated types - -### Build Process -- Consider adding the generation scripts to VS Code tasks or package.json scripts -- Add validation that generated files are up to date in CI/CD - -## Benefits of These Improvements - -1. **Single Source of Truth** - All configuration changes happen in `shared_config.py` -2. **Type Safety** - Reduce runtime errors from typos in field names -3. **Maintainability** - Adding new config fields requires minimal manual code changes -4. **Consistency** - Generated code ensures consistent patterns across Python/TypeScript -5. **Documentation** - Schema serves as living documentation of configuration options -- cgit v1.2.3 From 43def41747d3b75bb547b649a00f12653c3ae537 Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Tue, 22 Jul 2025 13:25:04 -0400 Subject: rm outdated test --- tests/test_configuration.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 tests/test_configuration.py diff --git a/tests/test_configuration.py b/tests/test_configuration.py deleted file mode 100644 index e69de29..0000000 -- cgit v1.2.3