From e54b7e2c5f3a736f248353317007f922771ab0c7 Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Mon, 21 Jul 2025 23:04:08 -0400 Subject: refactor: remove unused backend files and improve configuration handling in TypeScript --- backend/Dockerfile | 9 -- backend/Makefile | 14 -- backend/entrypoint.sh | 8 -- backend/src/main.c | 5 - generate_ts_config.py | 132 ++++++++++++++++++ py_modules/lsfg_vk/base_service.py | 44 +++++- py_modules/lsfg_vk/config_schema.py | 105 +++++++-------- py_modules/lsfg_vk/configuration.py | 78 +++-------- py_modules/lsfg_vk/installation.py | 34 ++--- shared_config.py | 106 +++++++++++++++ src/components/UsageInstructions.tsx | 22 +-- src/config/configSchema.ts | 252 +++++++++++++++-------------------- src/config/generatedConfigSchema.ts | 127 ++++++++++++++++++ 13 files changed, 597 insertions(+), 339 deletions(-) delete mode 100644 backend/Dockerfile delete mode 100644 backend/Makefile delete mode 100755 backend/entrypoint.sh delete mode 100644 backend/src/main.c create mode 100755 generate_ts_config.py create mode 100644 shared_config.py create mode 100644 src/config/generatedConfigSchema.ts diff --git a/backend/Dockerfile b/backend/Dockerfile deleted file mode 100644 index f46e170..0000000 --- a/backend/Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -# we support images for building with a vanilla SteamOS base, -# or versions with ootb support for rust or go -# developers can also customize these images via this Dockerfile -#FROM ghcr.io/steamdeckhomebrew/holo-toolchain-rust:latest -#FROM ghcr.io/steamdeckhomebrew/holo-toolchain-go:latest -FROM ghcr.io/steamdeckhomebrew/holo-base:latest - -# entrypoint.sh should always be located in the backend folder -ENTRYPOINT [ "/backend/entrypoint.sh" ] \ No newline at end of file diff --git a/backend/Makefile b/backend/Makefile deleted file mode 100644 index a1e5dc5..0000000 --- a/backend/Makefile +++ /dev/null @@ -1,14 +0,0 @@ -# This is the default target, which will be built when -# you invoke make -.PHONY: all -all: hello - -# This rule tells make how to build hello from hello.cpp -hello: - mkdir -p ./out - gcc -o ./out/hello ./src/main.c - -# This rule tells make to delete hello and hello.o -.PHONY: clean -clean: - rm -f hello \ No newline at end of file diff --git a/backend/entrypoint.sh b/backend/entrypoint.sh deleted file mode 100755 index af04d23..0000000 --- a/backend/entrypoint.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/sh -set -e - -echo "Container's IP address: `awk 'END{print $1}' /etc/hosts`" - -cd /backend - -make \ No newline at end of file diff --git a/backend/src/main.c b/backend/src/main.c deleted file mode 100644 index b3b8fed..0000000 --- a/backend/src/main.c +++ /dev/null @@ -1,5 +0,0 @@ -#include -int main() { - printf("Hello World\n"); - return 0; -} \ No newline at end of file diff --git a/generate_ts_config.py b/generate_ts_config.py new file mode 100755 index 0000000..7585ec6 --- /dev/null +++ b/generate_ts_config.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 +""" +Generate TypeScript configuration constants from shared Python config. +This script is run during development to sync the schemas. +""" + +import sys +from pathlib import Path + +# Add the parent directory to Python path to import shared_config +sys.path.insert(0, str(Path(__file__).parent)) + +from shared_config import CONFIG_SCHEMA_DEF, ConfigFieldType, get_field_names, get_defaults, get_field_types + + +def generate_typescript_config(): + """Generate TypeScript configuration constants""" + + ts_content = '''/** + * Auto-generated TypeScript configuration schema. + * DO NOT EDIT MANUALLY - Generated from shared_config.py + * + * To update this file, run: python3 generate_ts_config.py > 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 each field + for field_name, field_def in CONFIG_SCHEMA_DEF.items(): + field_type = field_def["fieldType"] + default_value = field_def["default"] + + # Format default value for TypeScript + if field_type == "string": + default_str = f'"{default_value}"' + elif field_type == "boolean": + default_str = "true" if default_value else "false" + else: + default_str = str(default_value) + + ts_content += f''' {field_name}: {{ + name: "{field_def["name"]}", + fieldType: ConfigFieldType.{field_type.upper()}, + default: {default_str}, + description: "{field_def["description"]}" + }}, +''' + + ts_content += '''}; + +// Type-safe configuration data structure +export interface ConfigurationData { +''' + + # Generate interface fields + for field_name, field_def in CONFIG_SCHEMA_DEF.items(): + field_type = field_def["fieldType"] + + # Map Python types to TypeScript types + ts_type_map = { + "string": "string", + "boolean": "boolean", + "integer": "number", + "float": "number" + } + + ts_type = ts_type_map[field_type] + ts_content += f' {field_name}: {ts_type};\n' + + ts_content += '''} + +// Helper functions +export function getFieldNames(): string[] { + return Object.keys(CONFIG_SCHEMA); +} + +export function getDefaults(): ConfigurationData { + return { +''' + + # Generate defaults object + for field_name, field_def in CONFIG_SCHEMA_DEF.items(): + default_value = field_def["default"] + field_type = field_def["fieldType"] + + if field_type == "string": + default_str = f'"{default_value}"' + elif field_type == "boolean": + default_str = "true" if default_value else "false" + else: + default_str = str(default_value) + + ts_content += f' {field_name}: {default_str},\n' + + ts_content += ''' }; +} + +export function getFieldTypes(): Record { + return { +''' + + # Generate field types object + for field_name, field_def in CONFIG_SCHEMA_DEF.items(): + ts_content += f' {field_name}: "{field_def["fieldType"]}",\n' + + ts_content += ''' }; +} +''' + + return ts_content + + +if __name__ == "__main__": + print(generate_typescript_config()) diff --git a/py_modules/lsfg_vk/base_service.py b/py_modules/lsfg_vk/base_service.py index b595b07..b684ec9 100644 --- a/py_modules/lsfg_vk/base_service.py +++ b/py_modules/lsfg_vk/base_service.py @@ -5,10 +5,13 @@ Base service class with common functionality. import os import shutil from pathlib import Path -from typing import Any, Optional +from typing import Any, Optional, TypeVar, Dict from .constants import LOCAL_LIB, LOCAL_SHARE_BASE, VULKAN_LAYER_DIR, SCRIPT_NAME, CONFIG_DIR, CONFIG_FILENAME +# Generic type for response dictionaries +ResponseType = TypeVar('ResponseType', bound=Dict[str, Any]) + class BaseService: """Base service class with common functionality""" @@ -90,3 +93,42 @@ class BaseService: except Exception: self.log.error(f"Failed to write to {path}") raise + + def _success_response(self, response_type: type, message: str = "", **kwargs) -> Any: + """Create a standardized success response + + Args: + response_type: The TypedDict response type to create + message: Success message + **kwargs: Additional response fields + + Returns: + Success response dict + """ + response = { + "success": True, + "message": message, + "error": None + } + response.update(kwargs) + return response + + def _error_response(self, response_type: type, error: str, message: str = "", **kwargs) -> Any: + """Create a standardized error response + + Args: + response_type: The TypedDict response type to create + error: Error description + message: Optional message + **kwargs: Additional response fields + + Returns: + Error response dict + """ + response = { + "success": False, + "message": message, + "error": error + } + response.update(kwargs) + return response diff --git a/py_modules/lsfg_vk/config_schema.py b/py_modules/lsfg_vk/config_schema.py index 4f036ff..39593ab 100644 --- a/py_modules/lsfg_vk/config_schema.py +++ b/py_modules/lsfg_vk/config_schema.py @@ -9,18 +9,28 @@ This module defines the complete configuration structure for lsfg-vk, managing T """ import re +import sys from typing import TypedDict, Dict, Any, Union, cast from dataclasses import dataclass from enum import Enum 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, get_field_names, get_defaults, get_field_types -class ConfigFieldType(Enum): - """Supported configuration field types""" - BOOLEAN = "boolean" - INTEGER = "integer" - FLOAT = "float" - STRING = "string" + +@dataclass +class ConfigField: + """Configuration field definition""" + name: str + field_type: ConfigFieldType + default: Union[bool, int, float, str] + description: str + + def get_toml_value(self, value: Union[bool, int, float, str]) -> Union[bool, int, float, str]: + """Get the value for TOML output""" + return value @dataclass @@ -36,51 +46,25 @@ class ConfigField: return value -# Configuration schema definition +# Use shared configuration schema as source of truth CONFIG_SCHEMA: Dict[str, ConfigField] = { - "dll": ConfigField( - name="dll", - field_type=ConfigFieldType.STRING, - default="", # Will be populated dynamically based on detection - description="specify where Lossless.dll is stored" - ), - - "multiplier": ConfigField( - name="multiplier", - field_type=ConfigFieldType.INTEGER, - default=1, - description="change the fps multiplier" - ), - - "flow_scale": ConfigField( - name="flow_scale", - field_type=ConfigFieldType.FLOAT, - default=0.8, - description="change the flow scale" - ), - - "performance_mode": ConfigField( - name="performance_mode", - field_type=ConfigFieldType.BOOLEAN, - default=True, - description="toggle performance mode" - ), - - "hdr_mode": ConfigField( - name="hdr_mode", - field_type=ConfigFieldType.BOOLEAN, - default=False, - description="enable hdr mode" - ), - - "experimental_present_mode": ConfigField( - name="experimental_present_mode", - field_type=ConfigFieldType.STRING, - default="fifo", - description="experimental: override vulkan present mode (fifo/mailbox/immediate)" - ), + 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() } +# Override DLL default to empty (will be populated dynamically) +CONFIG_SCHEMA["dll"] = ConfigField( + name="dll", + field_type=ConfigFieldType.STRING, + default="", # Will be populated dynamically based on detection + description="specify where Lossless.dll is stored" +) + # Fields that should ONLY be in the lsfg script, not in TOML config SCRIPT_ONLY_FIELDS = { "dxvk_frame_rate": ConfigField( @@ -128,10 +112,16 @@ class ConfigurationManager: @staticmethod def get_defaults() -> ConfigurationData: """Get default configuration values""" - return cast(ConfigurationData, { + # Use shared defaults and add script-only fields + shared_defaults = get_defaults() + + # Add script-only fields that aren't in the shared schema + script_defaults = { field.name: field.default - for field in COMPLETE_CONFIG_SCHEMA.values() - }) + for field in SCRIPT_ONLY_FIELDS.values() + } + + return cast(ConfigurationData, {**shared_defaults, **script_defaults}) @staticmethod def get_defaults_with_dll_detection(dll_detection_service=None) -> ConfigurationData: @@ -164,15 +154,18 @@ class ConfigurationManager: @staticmethod def get_field_names() -> list[str]: """Get ordered list of configuration field names""" - return list(COMPLETE_CONFIG_SCHEMA.keys()) + # Use shared field names and add script-only fields + shared_names = get_field_names() + script_names = list(SCRIPT_ONLY_FIELDS.keys()) + return shared_names + script_names @staticmethod def get_field_types() -> Dict[str, ConfigFieldType]: """Get field type mapping""" - return { - field.name: field.field_type - for field in CONFIG_SCHEMA.values() - } + # Use shared field types and add script-only field types + shared_types = {name: ConfigFieldType(type_str) for name, type_str in get_field_types().items()} + script_types = {field.name: field.field_type for field in SCRIPT_ONLY_FIELDS.values()} + return {**shared_types, **script_types} @staticmethod def validate_config(config: Dict[str, Any]) -> ConfigurationData: diff --git a/py_modules/lsfg_vk/configuration.py b/py_modules/lsfg_vk/configuration.py index da765e0..47d0ebc 100644 --- a/py_modules/lsfg_vk/configuration.py +++ b/py_modules/lsfg_vk/configuration.py @@ -43,22 +43,12 @@ class ConfigurationService(BaseService): # Merge TOML config with script values config = ConfigurationManager.merge_config_with_script(toml_config, script_values) - return { - "success": True, - "config": config, - "message": None, - "error": None - } + return self._success_response(ConfigurationResponse, config=config) except (OSError, IOError) as e: error_msg = f"Error reading lsfg config: {str(e)}" self.log.error(error_msg) - return { - "success": False, - "config": None, - "message": None, - "error": str(e) - } + return self._error_response(ConfigurationResponse, str(e), config=None) except Exception as e: error_msg = f"Error parsing config file: {str(e)}" self.log.error(error_msg) @@ -66,12 +56,9 @@ class ConfigurationService(BaseService): from .dll_detection import DllDetectionService dll_service = DllDetectionService(self.log) config = ConfigurationManager.get_defaults_with_dll_detection(dll_service) - return { - "success": True, - "config": config, - "message": f"Using default configuration due to parse error: {str(e)}", - "error": None - } + return self._success_response(ConfigurationResponse, + f"Using default configuration due to parse error: {str(e)}", + config=config) def update_config(self, dll: str, multiplier: int, flow_scale: float, performance_mode: bool, hdr_mode: bool, @@ -123,31 +110,18 @@ class ConfigurationService(BaseService): f"dxvk_frame_rate={dxvk_frame_rate}, " f"enable_wow64={enable_wow64}, disable_steamdeck_mode={disable_steamdeck_mode}") - return { - "success": True, - "config": config, - "message": "lsfg configuration updated successfully", - "error": None - } + 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 { - "success": False, - "config": None, - "message": None, - "error": str(e) - } + 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 { - "success": False, - "config": None, - "message": None, - "error": str(e) - } + return self._error_response(ConfigurationResponse, str(e), config=None) def update_dll_path(self, dll_path: str) -> ConfigurationResponse: """Update just the DLL path in the configuration @@ -183,22 +157,14 @@ class ConfigurationService(BaseService): self.log.info(f"Updated DLL path in lsfg configuration: '{dll_path}'") - return { - "success": True, - "config": config, - "message": f"DLL path updated to: {dll_path}", - "error": None - } + return self._success_response(ConfigurationResponse, + f"DLL path updated to: {dll_path}", + config=config) except Exception as e: error_msg = f"Error updating DLL path: {str(e)}" self.log.error(error_msg) - return { - "success": False, - "config": None, - "message": None, - "error": str(e) - } + return self._error_response(ConfigurationResponse, str(e), config=None) def update_lsfg_script(self, config: ConfigurationData) -> ConfigurationResponse: """Update the ~/lsfg launch script with current configuration @@ -217,22 +183,14 @@ class ConfigurationService(BaseService): self.log.info(f"Updated lsfg launch script at {self.lsfg_script_path}") - return { - "success": True, - "config": config, - "message": "Launch script updated successfully", - "error": None - } + return self._success_response(ConfigurationResponse, + "Launch script updated successfully", + config=config) except Exception as e: error_msg = f"Error updating launch script: {str(e)}" self.log.error(error_msg) - return { - "success": False, - "config": None, - "message": None, - "error": str(e) - } + return self._error_response(ConfigurationResponse, str(e), config=None) def _generate_script_content(self, config: ConfigurationData) -> str: """Generate the content for the ~/lsfg launch script diff --git a/py_modules/lsfg_vk/installation.py b/py_modules/lsfg_vk/installation.py index 5bfc88b..b340093 100644 --- a/py_modules/lsfg_vk/installation.py +++ b/py_modules/lsfg_vk/installation.py @@ -43,7 +43,7 @@ class InstallationService(BaseService): if not zip_path.exists(): error_msg = f"{ZIP_FILENAME} not found at {zip_path}" self.log.error(error_msg) - return {"success": False, "error": error_msg, "message": ""} + return self._error_response(InstallationResponse, error_msg, message="") # Create directories if they don't exist self._ensure_directories() @@ -58,17 +58,17 @@ class InstallationService(BaseService): self._create_lsfg_launch_script() self.log.info("lsfg-vk installed successfully") - return {"success": True, "message": "lsfg-vk installed successfully", "error": None} + return self._success_response(InstallationResponse, "lsfg-vk installed successfully") except (OSError, zipfile.BadZipFile, shutil.Error) as e: error_msg = f"Error installing lsfg-vk: {str(e)}" self.log.error(error_msg) - return {"success": False, "error": str(e), "message": ""} + return self._error_response(InstallationResponse, str(e), message="") except Exception as e: # Catch unexpected errors but log them separately error_msg = f"Unexpected error installing lsfg-vk: {str(e)}" self.log.error(error_msg) - return {"success": False, "error": str(e), "message": ""} + return self._error_response(InstallationResponse, str(e), message="") def _extract_and_install_files(self, zip_path: Path) -> None: """Extract zip file and install files to appropriate locations @@ -209,30 +209,20 @@ class InstallationService(BaseService): pass # Directory not empty or other error, ignore if not removed_files: - return { - "success": True, - "message": "No lsfg-vk files found to remove", - "removed_files": None, - "error": None - } + return self._success_response(UninstallationResponse, + "No lsfg-vk files found to remove", + removed_files=None) self.log.info("lsfg-vk uninstalled successfully") - return { - "success": True, - "message": f"lsfg-vk uninstalled successfully. Removed {len(removed_files)} files.", - "removed_files": removed_files, - "error": None - } + return self._success_response(UninstallationResponse, + f"lsfg-vk uninstalled successfully. Removed {len(removed_files)} files.", + removed_files=removed_files) except OSError as e: error_msg = f"Error uninstalling lsfg-vk: {str(e)}" self.log.error(error_msg) - return { - "success": False, - "message": "", - "removed_files": None, - "error": str(e) - } + return self._error_response(UninstallationResponse, str(e), + message="", removed_files=None) def cleanup_on_uninstall(self) -> None: """Clean up lsfg-vk files when the plugin is uninstalled""" diff --git a/shared_config.py b/shared_config.py new file mode 100644 index 0000000..46bfc84 --- /dev/null +++ b/shared_config.py @@ -0,0 +1,106 @@ +""" +Shared configuration schema constants. + +This file contains the canonical configuration schema that should be used +by both Python and TypeScript code. Any changes to the configuration +structure should be made here first. +""" + +from typing import Dict, Any, Union +from enum import Enum + + +class ConfigFieldType(str, Enum): + """Configuration field types - must match TypeScript enum""" + BOOLEAN = "boolean" + INTEGER = "integer" + FLOAT = "float" + STRING = "string" + + +# Canonical configuration schema - source of truth +CONFIG_SCHEMA_DEF = { + "dll": { + "name": "dll", + "fieldType": ConfigFieldType.STRING, + "default": "/games/Lossless Scaling/Lossless.dll", + "description": "specify where Lossless.dll is stored" + }, + + "multiplier": { + "name": "multiplier", + "fieldType": ConfigFieldType.INTEGER, + "default": 1, + "description": "change the fps multiplier" + }, + + "flow_scale": { + "name": "flow_scale", + "fieldType": ConfigFieldType.FLOAT, + "default": 0.8, + "description": "change the flow scale" + }, + + "performance_mode": { + "name": "performance_mode", + "fieldType": ConfigFieldType.BOOLEAN, + "default": True, + "description": "use a lighter model for FG (recommended for most games)" + }, + + "hdr_mode": { + "name": "hdr_mode", + "fieldType": ConfigFieldType.BOOLEAN, + "default": False, + "description": "enable HDR mode (only for games that support HDR)" + }, + + "experimental_present_mode": { + "name": "experimental_present_mode", + "fieldType": ConfigFieldType.STRING, + "default": "fifo", + "description": "override Vulkan present mode (may cause crashes)" + }, + + "dxvk_frame_rate": { + "name": "dxvk_frame_rate", + "fieldType": ConfigFieldType.INTEGER, + "default": 0, + "description": "base framerate cap for DirectX games before frame multiplier" + }, + + "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)" + }, + + "disable_steamdeck_mode": { + "name": "disable_steamdeck_mode", + "fieldType": ConfigFieldType.BOOLEAN, + "default": False, + "description": "disable Steam Deck mode (unlocks hidden settings in some games)" + } +} + + +def get_field_names() -> list[str]: + """Get ordered list of configuration field names""" + return list(CONFIG_SCHEMA_DEF.keys()) + + +def get_defaults() -> Dict[str, Union[bool, int, float, str]]: + """Get default configuration values""" + return { + field_name: field_def["default"] + for field_name, field_def in CONFIG_SCHEMA_DEF.items() + } + + +def get_field_types() -> Dict[str, str]: + """Get field type mapping""" + return { + field_name: field_def["fieldType"].value + for field_name, field_def in CONFIG_SCHEMA_DEF.items() + } diff --git a/src/components/UsageInstructions.tsx b/src/components/UsageInstructions.tsx index bcf258b..0c27517 100644 --- a/src/components/UsageInstructions.tsx +++ b/src/components/UsageInstructions.tsx @@ -5,7 +5,7 @@ interface UsageInstructionsProps { config: ConfigurationData; } -export function UsageInstructions({ config }: UsageInstructionsProps) { +export function UsageInstructions({ config: _config }: UsageInstructionsProps) { return ( <> @@ -56,26 +56,6 @@ export function UsageInstructions({ config }: UsageInstructionsProps) { - {/* -
- {`Current Configuration: -• DLL Path: ${config.dll} -• Multiplier: ${config.multiplier}x -• Flow Scale: ${Math.round(config.flow_scale * 100)}% -• Performance Mode: ${config.performance_mode ? "Yes" : "No"} -• HDR Mode: ${config.hdr_mode ? "Yes" : "No"} -• Present Mode: ${config.experimental_present_mode || "FIFO (VSync)"} -• DXVK Frame Rate: ${config.dxvk_frame_rate > 0 ? `${config.dxvk_frame_rate} FPS` : "Off"}`} -
-
*/} -
= { - dll: { - name: "dll", - fieldType: ConfigFieldType.STRING, - default: "/games/Lossless Scaling/Lossless.dll", - description: "specify where Lossless.dll is stored" - }, - - multiplier: { - name: "multiplier", - fieldType: ConfigFieldType.INTEGER, - default: 1, - description: "change the fps multiplier" - }, - - flow_scale: { - name: "flow_scale", - fieldType: ConfigFieldType.FLOAT, - default: 0.8, - description: "change the flow scale" - }, - - performance_mode: { - name: "performance_mode", - fieldType: ConfigFieldType.BOOLEAN, - default: true, - description: "toggle performance mode" - }, - - hdr_mode: { - name: "hdr_mode", - fieldType: ConfigFieldType.BOOLEAN, - default: false, - description: "enable hdr in games that support it" - }, - - experimental_present_mode: { - name: "experimental_present_mode", - fieldType: ConfigFieldType.STRING, - default: "fifo", - description: "experimental: override vulkan present mode (fifo/mailbox/immediate)" - }, - - dxvk_frame_rate: { - name: "dxvk_frame_rate", - fieldType: ConfigFieldType.INTEGER, - default: 0, - description: "Base framerate cap for DirectX games, before frame multiplier (0 = disabled, requires game re-launch)" - }, - - 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)" - }, - - disable_steamdeck_mode: { - name: "disable_steamdeck_mode", - fieldType: ConfigFieldType.BOOLEAN, - default: false, - description: "disable Steam Deck mode (unlocks hidden settings in some games)" - } -}; +/** + * Configuration management class + * Handles CRUD operations for plugin configuration + */ +export class ConfigurationManager { + private static instance: ConfigurationManager; + private _config: ConfigurationData | null = null; -// Type-safe configuration data structure -export interface ConfigurationData { - dll: string; - multiplier: number; - flow_scale: number; - performance_mode: boolean; - hdr_mode: boolean; - experimental_present_mode: string; - dxvk_frame_rate: number; - enable_wow64: boolean; - disable_steamdeck_mode: boolean; -} + // 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() {} + + static getInstance(): ConfigurationManager { + if (!ConfigurationManager.instance) { + ConfigurationManager.instance = new ConfigurationManager(); + } + return ConfigurationManager.instance; + } -// Centralized configuration manager -export class ConfigurationManager { /** * Get default configuration values */ static getDefaults(): ConfigurationData { - const defaults = {} as ConfigurationData; - Object.values(CONFIG_SCHEMA).forEach(field => { - (defaults as any)[field.name] = field.default; - }); - return defaults; + return getDefaults(); } /** - * Get ordered list of configuration field names + * Create args array from config object for lsfg API calls */ - static getFieldNames(): string[] { - return Object.keys(CONFIG_SCHEMA); + static createArgsFromConfig(config: ConfigurationData): [string, number, number, boolean, boolean, string, number, 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 + ]; } /** - * Get field type mapping + * Load configuration from backend */ - static getFieldTypes(): Record { - return Object.values(CONFIG_SCHEMA).reduce((acc, field) => { - acc[field.name] = field.fieldType; - return acc; - }, {} as Record); + async loadConfig(): Promise { + try { + const result = await this.getConfiguration(); + if (result.success && result.data) { + this._config = result.data; + return this._config; + } else { + throw new Error(result.error || 'Failed to load configuration'); + } + } catch (error) { + console.error('Error loading configuration:', error); + throw error; + } } /** - * Create ordered arguments array from configuration object + * Save configuration to backend */ - static createArgsFromConfig(config: ConfigurationData): (boolean | number | string)[] { - return this.getFieldNames().map(fieldName => - config[fieldName as keyof ConfigurationData] - ); + async saveConfig(config: ConfigurationData): Promise { + try { + const result = await this.setConfiguration({ config_data: config }); + if (result.success) { + this._config = config; + } else { + throw new Error(result.error || 'Failed to save configuration'); + } + } catch (error) { + console.error('Error saving configuration:', error); + throw error; + } } /** - * Validate configuration object against schema + * Update a single configuration field */ - static validateConfig(config: Partial): ConfigurationData { - const defaults = this.getDefaults(); - const validated = { ...defaults }; - - Object.entries(CONFIG_SCHEMA).forEach(([fieldName, fieldDef]) => { - const value = config[fieldName as keyof ConfigurationData]; - if (value !== undefined) { - // Type validation - if (fieldDef.fieldType === ConfigFieldType.BOOLEAN) { - (validated as any)[fieldName] = Boolean(value); - } else if (fieldDef.fieldType === ConfigFieldType.INTEGER) { - (validated as any)[fieldName] = parseInt(String(value), 10); - } else if (fieldDef.fieldType === ConfigFieldType.FLOAT) { - (validated as any)[fieldName] = parseFloat(String(value)); - } else if (fieldDef.fieldType === ConfigFieldType.STRING) { - (validated as any)[fieldName] = String(value); - } - } - }); - - return validated; + async updateField(fieldName: keyof ConfigurationData, value: any): Promise { + if (!this._config) { + await this.loadConfig(); + } + + const updatedConfig = { + ...this._config!, + [fieldName]: value + }; + + await this.saveConfig(updatedConfig); } /** - * Get configuration field definition + * Get current configuration (cached) */ - static getFieldDef(fieldName: string): ConfigField | undefined { - return CONFIG_SCHEMA[fieldName]; + getConfig(): ConfigurationData | null { + return this._config; } /** - * Get all field definitions + * Reset configuration to defaults */ - static getAllFieldDefs(): ConfigField[] { - return Object.values(CONFIG_SCHEMA); + async resetToDefaults(): Promise { + try { + const result = await this.resetConfiguration(); + if (result.success && result.data) { + this._config = result.data; + return this._config; + } else { + throw new Error(result.error || 'Failed to reset configuration'); + } + } catch (error) { + console.error('Error resetting configuration:', error); + throw error; + } } } + +// Export singleton instance +export const configManager = ConfigurationManager.getInstance(); diff --git a/src/config/generatedConfigSchema.ts b/src/config/generatedConfigSchema.ts new file mode 100644 index 0000000..d862ad2 --- /dev/null +++ b/src/config/generatedConfigSchema.ts @@ -0,0 +1,127 @@ +/** + * Auto-generated TypeScript configuration schema. + * DO NOT EDIT MANUALLY - Generated from shared_config.py + * + * To update this file, run: python3 generate_ts_config.py > 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 = { + dll: { + name: "dll", + fieldType: ConfigFieldType.STRING, + default: "/games/Lossless Scaling/Lossless.dll", + description: "specify where Lossless.dll is stored" + }, + multiplier: { + name: "multiplier", + fieldType: ConfigFieldType.INTEGER, + default: 1, + description: "change the fps multiplier" + }, + flow_scale: { + name: "flow_scale", + fieldType: ConfigFieldType.FLOAT, + default: 0.8, + description: "change the flow scale" + }, + performance_mode: { + name: "performance_mode", + fieldType: ConfigFieldType.BOOLEAN, + default: true, + description: "use a lighter model for FG (recommended for most games)" + }, + hdr_mode: { + name: "hdr_mode", + fieldType: ConfigFieldType.BOOLEAN, + default: false, + description: "enable HDR mode (only for games that support HDR)" + }, + experimental_present_mode: { + name: "experimental_present_mode", + fieldType: ConfigFieldType.STRING, + default: "fifo", + description: "override Vulkan present mode (may cause crashes)" + }, + dxvk_frame_rate: { + name: "dxvk_frame_rate", + fieldType: ConfigFieldType.INTEGER, + default: 0, + description: "base framerate cap for DirectX games before frame multiplier" + }, + 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)" + }, + disable_steamdeck_mode: { + name: "disable_steamdeck_mode", + fieldType: ConfigFieldType.BOOLEAN, + default: false, + description: "disable Steam Deck mode (unlocks hidden settings in some games)" + }, +}; + +// Type-safe configuration data structure +export interface ConfigurationData { + dll: string; + multiplier: number; + flow_scale: number; + performance_mode: boolean; + hdr_mode: boolean; + experimental_present_mode: string; + dxvk_frame_rate: number; + enable_wow64: boolean; + disable_steamdeck_mode: boolean; +} + +// Helper functions +export function getFieldNames(): string[] { + return Object.keys(CONFIG_SCHEMA); +} + +export function getDefaults(): ConfigurationData { + return { + dll: "/games/Lossless Scaling/Lossless.dll", + multiplier: 1, + flow_scale: 0.8, + performance_mode: true, + hdr_mode: false, + experimental_present_mode: "fifo", + dxvk_frame_rate: 0, + enable_wow64: false, + disable_steamdeck_mode: false, + }; +} + +export function getFieldTypes(): Record { + return { + dll: "ConfigFieldType.STRING", + multiplier: "ConfigFieldType.INTEGER", + flow_scale: "ConfigFieldType.FLOAT", + performance_mode: "ConfigFieldType.BOOLEAN", + hdr_mode: "ConfigFieldType.BOOLEAN", + experimental_present_mode: "ConfigFieldType.STRING", + dxvk_frame_rate: "ConfigFieldType.INTEGER", + enable_wow64: "ConfigFieldType.BOOLEAN", + disable_steamdeck_mode: "ConfigFieldType.BOOLEAN", + }; +} + -- cgit v1.2.3