summaryrefslogtreecommitdiff
path: root/py_modules/lsfg_vk/configuration.py
diff options
context:
space:
mode:
authorKurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com>2026-09-12 21:29:08 -0400
committerGitHub <noreply@github.com>2026-09-12 21:29:08 -0400
commite580c6bd60c92af36f92d4c29b5d9a44f73d47d7 (patch)
tree7fc6799626519e5b01fb137f5440c99fda5faca7 /py_modules/lsfg_vk/configuration.py
parente997a3fb74fa70f5e60b60807fb0897120e98313 (diff)
parent81feb03288166755545401b9df2ff2c9bc3ae7d7 (diff)
downloaddecky-lsfg-vk-e580c6bd60c92af36f92d4c29b5d9a44f73d47d7.tar.gz
decky-lsfg-vk-e580c6bd60c92af36f92d4c29b5d9a44f73d47d7.zip
Merge pull request #264 from xXJSONDeruloXx/chore/migrate
the big one
Diffstat (limited to 'py_modules/lsfg_vk/configuration.py')
-rw-r--r--py_modules/lsfg_vk/configuration.py627
1 files changed, 197 insertions, 430 deletions
diff --git a/py_modules/lsfg_vk/configuration.py b/py_modules/lsfg_vk/configuration.py
index fce3738..3c65972 100644
--- a/py_modules/lsfg_vk/configuration.py
+++ b/py_modules/lsfg_vk/configuration.py
@@ -1,446 +1,213 @@
-"""
-Configuration service for TOML-based lsfg configuration management.
-"""
-
-from pathlib import Path
-from typing import Dict, Any
+import re
+from typing import Any, Dict
from .base_service import BaseService
-from .config_schema import ConfigurationManager, CONFIG_SCHEMA, ProfileData, DEFAULT_PROFILE_NAME
-from .config_schema_generated import ConfigurationData, get_script_generation_logic
-from .constants import ARMADA_DEVICE_ENV, ARMADA_GAME_LAUNCH
-from .types import ConfigurationResponse, ProfilesResponse, ProfileResponse
+from .config_schema import ConfigurationManager, ProfileData
+from .runtime_service import RuntimeService
class ConfigurationService(BaseService):
- """Service for managing TOML-based lsfg configuration"""
-
- def get_config(self) -> ConfigurationResponse:
- """Read current TOML configuration merged with launch script environment variables
-
- Returns:
- ConfigurationResponse with current configuration or error
- """
+ FLATPAK_PROFILE_PREFIX = "flatpak:"
+
+ def __init__(self, logger=None, runtime_service: RuntimeService = None):
+ super().__init__(logger)
+ self.runtime_service = runtime_service or RuntimeService(logger=self.log)
+
+ def _default_data(self) -> ProfileData:
+ return {"profiles": {}, "global_config": {"dll": "", "no_fp16": False}}
+
+ def _get_profile_data(self) -> ProfileData:
+ if not self.config_file_path.exists():
+ return self._default_data()
+ return ConfigurationManager.parse_toml_content_multi_profile(self.config_file_path.read_text(encoding="utf-8"))
+
+ def _save_profile_data(self, data: ProfileData) -> None:
+ content = ConfigurationManager.generate_toml_content_multi_profile(data)
+ self.runtime_service.validate_config_content(content)
+ self._write_file(self.config_file_path, content, 0o644)
+
+ @staticmethod
+ def _profile_name(data: ProfileData, appid: str, game_name: str) -> str:
+ name = str(game_name).strip()
+ if not name:
+ raise ValueError("game name is required")
+ existing = data["profiles"].get(name)
+ if existing is not None and str(appid) not in existing.get("active_in", []):
+ name = f"{name} ({appid})"
+ return name
+
+ @staticmethod
+ def _profile_for_appid(data: ProfileData, appid: str):
+ return next(
+ (
+ (name, profile)
+ for name, profile in data["profiles"].items()
+ if str(appid) in profile.get("active_in", [])
+ ),
+ (None, None),
+ )
+
+ @classmethod
+ def flatpak_profile_name(cls, app_id: str) -> str:
+ value = str(app_id).strip()
+ if not value:
+ raise ValueError("Flatpak application ID is required")
+ return f"{cls.FLATPAK_PROFILE_PREFIX}{value}"
+
+ @staticmethod
+ def _public_config(config: Dict[str, Any]) -> Dict[str, Any]:
+ return ConfigurationManager.validate_config(config)
+
+ def get_game_configs(self) -> Dict[str, Any]:
try:
- if not self.config_file_path.exists():
- from .dll_detection import DllDetectionService
- dll_service = DllDetectionService(self.log)
- toml_config = ConfigurationManager.get_defaults_with_dll_detection(dll_service)
- else:
- content = self.config_file_path.read_text(encoding='utf-8')
- toml_config = ConfigurationManager.parse_toml_content(content)
-
- script_values = {}
- if self.lsfg_script_path.exists():
- try:
- script_content = self.lsfg_script_path.read_text(encoding='utf-8')
- script_values = ConfigurationManager.parse_script_content(script_content)
- self.log.info(f"Parsed script values: {script_values}")
- except Exception as e:
- self.log.warning(f"Failed to parse launch script: {str(e)}")
-
- config = ConfigurationManager.merge_config_with_script(toml_config, script_values)
-
- 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 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)
- from .dll_detection import DllDetectionService
- dll_service = DllDetectionService(self.log)
- config = ConfigurationManager.get_defaults_with_dll_detection(dll_service)
- return self._success_response(ConfigurationResponse,
- 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
- """
+ data = self._get_profile_data()
+ games = []
+ for name, raw in data["profiles"].items():
+ active_in = raw.get("active_in", [])
+ if len(active_in) != 1 or not re.fullmatch(r"-?[0-9]+", str(active_in[0])):
+ continue
+ games.append({"appid": str(active_in[0]), "profile": name, "config": self._public_config(raw)})
+ return self._success_response(dict, global_config=dict(data["global_config"]), games=games)
+ except Exception as error:
+ self.log.error(f"Error reading game configs: {error}")
+ return self._error_response(dict, str(error), games=[])
+
+ def update_global_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
try:
- profile_data = self._get_profile_data()
- current_profile = profile_data["current_profile"]
-
- return self.update_profile_config(current_profile, 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_lsfg_script(self, config: ConfigurationData) -> ConfigurationResponse:
- """Update the ~/lsfg launch script with current configuration
-
- Args:
- config: Configuration data to apply to the script
-
- Returns:
- ConfigurationResponse indicating success or failure
- """
+ data = self._get_profile_data()
+ merged_config = {**data["global_config"], **config}
+ validated = self._public_config(merged_config)
+ data["global_config"] = {
+ "dll": validated["dll"],
+ "no_fp16": validated["no_fp16"],
+ }
+ self._save_profile_data(data)
+ return self._success_response(dict, global_config=dict(data["global_config"]))
+ except Exception as error:
+ return self._error_response(dict, str(error), global_config=None)
+
+ def update_game_config(self, appid: str, game_name: str, config: Dict[str, Any]) -> Dict[str, Any]:
try:
- script_content = self._generate_script_content(config)
-
- self._write_file(self.lsfg_script_path, script_content, 0o755)
-
- self.log.info(f"Updated lsfg launch script at {self.lsfg_script_path}")
-
- 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 self._error_response(ConfigurationResponse, str(e), config=None)
-
- def _generate_script_content(self, config: ConfigurationData) -> str:
- """Generate the content for the ~/lsfg launch script
-
- Args:
- config: Configuration data to apply to the script
-
- Returns:
- The complete script content as a string
- """
- lines = [
- "#!/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",
- ]
-
- generate_script_lines = get_script_generation_logic()
- lines.extend(generate_script_lines(config))
-
- lines.append("export LSFG_PROCESS=decky-lsfg-vk")
- lines.extend(self._generate_game_launch_lines())
-
- return "\n".join(lines) + "\n"
-
- def _generate_script_content_for_profile(self, profile_data: ProfileData) -> str:
- """Generate the content for the ~/lsfg launch script with profile support
-
- Args:
- profile_data: Profile data containing current profile and configurations
-
- Returns:
- The complete script content as a string
- """
- current_profile = profile_data["current_profile"]
- config = profile_data["profiles"].get(current_profile, ConfigurationManager.get_defaults())
-
- merged_config = dict(config)
- for field_name, value in profile_data["global_config"].items():
- merged_config[field_name] = value
-
- lines = [
- "#!/bin/bash",
- f"# Current profile: {current_profile}",
- ]
-
- generate_script_lines = get_script_generation_logic()
- lines.extend(generate_script_lines(merged_config))
-
- lines.append(f"export LSFG_PROCESS={current_profile}")
- lines.extend(self._generate_game_launch_lines())
-
- return "\n".join(lines) + "\n"
+ data = self._get_profile_data()
+ old_name, _ = self._profile_for_appid(data, appid)
+ name = self._profile_name(data, appid, game_name)
+ merged_config = {**data["global_config"], **{key: value for key, value in config.items() if key != "no_fp16"}}
+ if not config.get("dll"):
+ merged_config["dll"] = data["global_config"].get("dll", "")
+ validated = self._public_config(merged_config)
+ validated["active_in"] = [str(appid)]
+ if old_name and old_name != name:
+ data["profiles"].pop(old_name, None)
+ data["profiles"][name] = validated
+ self._save_profile_data(data)
+ return self._success_response(dict, appid=str(appid), config=validated)
+ except Exception as error:
+ return self._error_response(dict, str(error), appid=str(appid), config=None)
- @staticmethod
- def _generate_game_launch_lines() -> list[str]:
- """Generate a portable exec block with Armada's host wrapper."""
- device_env = ARMADA_DEVICE_ENV.as_posix()
- game_launch = ARMADA_GAME_LAUNCH.as_posix()
- return [
- f'armada_game_launch="{game_launch}"',
- 'for argument in "$@"; do',
- ' if [ "$argument" = "$armada_game_launch" ]; then',
- ' exec "$@"',
- " fi",
- "done",
- f'if [ -f "{device_env}" ] && [ -x "$armada_game_launch" ]; then',
- ' exec "$armada_game_launch" "$@"',
- "fi",
- 'exec "$@"',
- ]
-
- def _get_profile_data(self) -> ProfileData:
- """Get current profile data from config file"""
- if not self.config_file_path.exists():
- from .dll_detection import DllDetectionService
- dll_service = DllDetectionService(self.log)
- default_config = ConfigurationManager.get_defaults_with_dll_detection(dll_service)
- return ProfileData(
- current_profile=DEFAULT_PROFILE_NAME,
- profiles={DEFAULT_PROFILE_NAME: default_config},
- global_config={
- "dll": default_config.get("dll", ""),
- "no_fp16": False
- }
- )
-
- content = self.config_file_path.read_text(encoding='utf-8')
- return ConfigurationManager.parse_toml_content_multi_profile(content)
-
- def _save_profile_data(self, profile_data: ProfileData) -> None:
- """Save profile data to config file"""
- toml_content = ConfigurationManager.generate_toml_content_multi_profile(profile_data)
-
- self.config_dir.mkdir(parents=True, exist_ok=True)
-
- self._write_file(self.config_file_path, toml_content, 0o644)
-
- def get_profiles(self) -> ProfilesResponse:
- """Get list of all profiles and current profile
-
- Returns:
- ProfilesResponse with profile list and current profile
- """
+ def get_flatpak_config(self, app_id: str) -> Dict[str, Any]:
try:
- profile_data = self._get_profile_data()
-
- return self._success_response(ProfilesResponse,
- "Profiles retrieved successfully",
- profiles=list(profile_data["profiles"].keys()),
- current_profile=profile_data["current_profile"])
-
- except Exception as e:
- error_msg = f"Error getting profiles: {str(e)}"
- self.log.error(error_msg)
- return self._error_response(ProfilesResponse, str(e),
- profiles=None, current_profile=None)
-
- def create_profile(self, profile_name: str, source_profile: str = None) -> ProfileResponse:
- """Create a new profile
-
- Args:
- profile_name: Name for the new profile (spaces will be converted to dashes)
- source_profile: Optional source profile to copy from (default: current profile)
-
- Returns:
- ProfileResponse with success status and the normalized profile name
- """
+ data = self._get_profile_data()
+ name = self.flatpak_profile_name(app_id)
+ raw = data["profiles"].get(name)
+ return self._success_response(
+ dict,
+ app_id=str(app_id),
+ profile=name,
+ exists=raw is not None,
+ config=self._public_config(raw) if raw is not None else None,
+ global_config=dict(data["global_config"]),
+ )
+ except Exception as error:
+ return self._error_response(dict, str(error), app_id=str(app_id), config=None, exists=False)
+
+ def update_flatpak_config(self, app_id: str, config: Dict[str, Any]) -> Dict[str, Any]:
try:
- profile_data = self._get_profile_data()
-
- if not source_profile:
- source_profile = profile_data["current_profile"]
-
- # Get the normalized name that will be used for storage
- normalized_name = ConfigurationManager.normalize_profile_name(profile_name)
-
- new_profile_data = ConfigurationManager.create_profile(profile_data, profile_name, source_profile)
-
- self._save_profile_data(new_profile_data)
-
- self.log.info(f"Created profile '{normalized_name}' from '{source_profile}'")
-
- # Return the normalized name so frontend can use the actual stored name
- return self._success_response(ProfileResponse,
- f"Profile '{normalized_name}' created successfully",
- profile_name=normalized_name)
-
- except ValueError as e:
- error_msg = f"Invalid profile operation: {str(e)}"
- self.log.error(error_msg)
- return self._error_response(ProfileResponse, str(e), profile_name=None)
- except Exception as e:
- error_msg = f"Error creating profile: {str(e)}"
- self.log.error(error_msg)
- return self._error_response(ProfileResponse, str(e), profile_name=None)
-
- def delete_profile(self, profile_name: str) -> ProfileResponse:
- """Delete a profile
-
- Args:
- profile_name: Name of the profile to delete
-
- Returns:
- ProfileResponse with success status
- """
+ data = self._get_profile_data()
+ name = self.flatpak_profile_name(app_id)
+ merged_config = {**data["global_config"], **{key: value for key, value in config.items() if key != "no_fp16"}}
+ if not config.get("dll"):
+ merged_config["dll"] = data["global_config"].get("dll", "")
+ validated = self._public_config(merged_config)
+ validated["active_in"] = []
+ data["profiles"][name] = validated
+ self._save_profile_data(data)
+ return self._success_response(dict, app_id=str(app_id), profile=name, exists=True, config=validated)
+ except Exception as error:
+ return self._error_response(dict, str(error), app_id=str(app_id), config=None, exists=False)
+
+ def reset_flatpak_config(self, app_id: str) -> Dict[str, Any]:
try:
- profile_data = self._get_profile_data()
-
- new_profile_data = ConfigurationManager.delete_profile(profile_data, profile_name)
-
- self._save_profile_data(new_profile_data)
-
- script_result = self.update_lsfg_script_from_profile_data(new_profile_data)
- if not script_result["success"]:
- self.log.warning(f"Failed to update launch script: {script_result['error']}")
-
- self.log.info(f"Deleted profile '{profile_name}'")
-
- return self._success_response(ProfileResponse,
- f"Profile '{profile_name}' deleted successfully",
- profile_name=profile_name)
-
- except ValueError as e:
- error_msg = f"Invalid profile operation: {str(e)}"
- self.log.error(error_msg)
- return self._error_response(ProfileResponse, str(e), profile_name=None)
- except Exception as e:
- error_msg = f"Error deleting profile: {str(e)}"
- self.log.error(error_msg)
- return self._error_response(ProfileResponse, str(e), profile_name=None)
-
- def rename_profile(self, old_name: str, new_name: str) -> ProfileResponse:
- """Rename a profile
-
- Args:
- old_name: Current profile name
- new_name: New profile name (spaces will be converted to dashes)
-
- Returns:
- ProfileResponse with success status and the normalized profile name
- """
+ data = self._get_profile_data()
+ name = self.flatpak_profile_name(app_id)
+ data["profiles"].pop(name, None)
+ self._save_profile_data(data)
+ return self._success_response(dict, app_id=str(app_id), profile=name, exists=False)
+ except Exception as error:
+ return self._error_response(dict, str(error), app_id=str(app_id), config=None, exists=False)
+
+ def reset_all_flatpak_configs(self) -> Dict[str, Any]:
try:
- profile_data = self._get_profile_data()
-
- # Get the normalized name that will be used for storage
- normalized_name = ConfigurationManager.normalize_profile_name(new_name)
-
- new_profile_data = ConfigurationManager.rename_profile(profile_data, old_name, new_name)
-
- self._save_profile_data(new_profile_data)
-
- script_result = self.update_lsfg_script_from_profile_data(new_profile_data)
- if not script_result["success"]:
- self.log.warning(f"Failed to update launch script: {script_result['error']}")
-
- self.log.info(f"Renamed profile '{old_name}' to '{normalized_name}'")
-
- # Return the normalized name so frontend can use the actual stored name
- return self._success_response(ProfileResponse,
- f"Profile renamed from '{old_name}' to '{normalized_name}' successfully",
- profile_name=normalized_name)
-
- except ValueError as e:
- error_msg = f"Invalid profile operation: {str(e)}"
- self.log.error(error_msg)
- return self._error_response(ProfileResponse, str(e), profile_name=None)
- except Exception as e:
- error_msg = f"Error renaming profile: {str(e)}"
- self.log.error(error_msg)
- return self._error_response(ProfileResponse, str(e), profile_name=None)
-
- def set_current_profile(self, profile_name: str) -> ProfileResponse:
- """Set the current active profile
-
- Args:
- profile_name: Name of the profile to set as current
-
- Returns:
- ProfileResponse with success status
- """
+ data = self._get_profile_data()
+ data["profiles"] = {
+ name: profile
+ for name, profile in data["profiles"].items()
+ if not name.startswith(self.FLATPAK_PROFILE_PREFIX)
+ }
+ self._save_profile_data(data)
+ return self._success_response(dict, global_config=dict(data["global_config"]))
+ except Exception as error:
+ return self._error_response(dict, str(error))
+
+ def reset_game_config(self, appid: str) -> Dict[str, Any]:
try:
- profile_data = self._get_profile_data()
-
- new_profile_data = ConfigurationManager.set_current_profile(profile_data, profile_name)
-
- self._save_profile_data(new_profile_data)
-
- script_result = self.update_lsfg_script_from_profile_data(new_profile_data)
- if not script_result["success"]:
- self.log.warning(f"Failed to update launch script: {script_result['error']}")
-
- self.log.info(f"Set current profile to '{profile_name}'")
-
- return self._success_response(ProfileResponse,
- f"Current profile set to '{profile_name}' successfully",
- profile_name=profile_name)
-
- except ValueError as e:
- error_msg = f"Invalid profile operation: {str(e)}"
- self.log.error(error_msg)
- return self._error_response(ProfileResponse, str(e), profile_name=None)
- except Exception as e:
- error_msg = f"Error setting current profile: {str(e)}"
- self.log.error(error_msg)
- return self._error_response(ProfileResponse, str(e), profile_name=None)
-
- def update_profile_config(self, profile_name: str, config: ConfigurationData) -> ConfigurationResponse:
- """Update configuration for a specific profile
-
- Args:
- profile_name: Name of the profile to update
- config: Configuration data to apply
-
- Returns:
- ConfigurationResponse with success status
- """
+ data = self._get_profile_data()
+ name, _ = self._profile_for_appid(data, appid)
+ if name:
+ data["profiles"].pop(name, None)
+ self._save_profile_data(data)
+ return self._success_response(dict, appid=str(appid), exists=False)
+ except Exception as error:
+ return self._error_response(dict, str(error), appid=str(appid), config=None)
+
+ def reset_game_configs(self, appids: list[str]) -> Dict[str, Any]:
try:
- profile_data = self._get_profile_data()
-
- if profile_name not in profile_data["profiles"]:
- return self._error_response(ConfigurationResponse,
- f"Profile '{profile_name}' does not exist",
- config=None)
-
- # Update the profile's config
- profile_data["profiles"][profile_name] = config
-
- # Update global config fields if they're in the config
- for field_name in ["dll", "no_fp16"]:
- if field_name in config:
- profile_data["global_config"][field_name] = config[field_name]
-
- self._save_profile_data(profile_data)
-
- if profile_name == profile_data["current_profile"]:
- script_result = self.update_lsfg_script_from_profile_data(profile_data)
- if not script_result["success"]:
- self.log.warning(f"Failed to update launch script: {script_result['error']}")
-
- field_values = ", ".join(f"{k}={repr(v)}" for k, v in config.items())
- self.log.info(f"Updated profile '{profile_name}' configuration: {field_values}")
-
- return self._success_response(ConfigurationResponse,
- f"Profile '{profile_name}' configuration updated successfully",
- config=config)
-
- except Exception as e:
- error_msg = f"Error updating profile configuration: {str(e)}"
- self.log.error(error_msg)
- return self._error_response(ConfigurationResponse, str(e), config=None)
-
- def update_lsfg_script_from_profile_data(self, profile_data: ProfileData) -> ConfigurationResponse:
- """Update the ~/lsfg launch script from profile data
-
- Args:
- profile_data: Profile data to apply to the script
-
- Returns:
- ConfigurationResponse indicating success or failure
- """
+ if not isinstance(appids, list):
+ raise ValueError("appids must be a list")
+ requested = set()
+ for appid in appids:
+ if isinstance(appid, bool) or not isinstance(appid, (str, int)):
+ raise ValueError("appids must contain only strings or integers")
+ value = str(appid)
+ if not re.fullmatch(r"-?[0-9]+", value):
+ raise ValueError("appids must contain only numeric App IDs")
+ requested.add(value)
+
+ data = self._get_profile_data()
+ data["profiles"] = {
+ name: profile
+ for name, profile in data["profiles"].items()
+ if not (
+ len(profile.get("active_in", [])) == 1
+ and str(profile["active_in"][0]) in requested
+ )
+ }
+ self._save_profile_data(data)
+ return self._success_response(dict, global_config=dict(data["global_config"]), games=[])
+ except Exception as error:
+ return self._error_response(dict, str(error), games=[])
+
+ def reset_all_game_configs(self) -> Dict[str, Any]:
try:
- script_content = self._generate_script_content_for_profile(profile_data)
-
- # Write the script file
- self._write_file(self.lsfg_script_path, script_content, 0o755)
-
- self.log.info(f"Updated lsfg launch script at {self.lsfg_script_path} for profile '{profile_data['current_profile']}'")
-
- # Get current profile config for response
- current_config = profile_data["profiles"].get(profile_data["current_profile"], ConfigurationManager.get_defaults())
-
- return self._success_response(ConfigurationResponse,
- "Launch script updated successfully",
- config=current_config)
-
- except Exception as e:
- error_msg = f"Error updating launch script: {str(e)}"
- self.log.error(error_msg)
- return self._error_response(ConfigurationResponse, str(e), config=None)
+ data = self._get_profile_data()
+ data["profiles"] = {
+ name: profile
+ for name, profile in data["profiles"].items()
+ if not (
+ len(profile.get("active_in", [])) == 1
+ and re.fullmatch(r"-?[0-9]+", str(profile.get("active_in", [""])[0]))
+ )
+ }
+ self._save_profile_data(data)
+ return self._success_response(dict, global_config=dict(data["global_config"]), games=[])
+ except Exception as error:
+ return self._error_response(dict, str(error), games=[])