We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Installer didnt Start Errors in .py
i want to install some stuff like kotor with steam but get everytime the same error.

File "/usr/lib/python3.10/site-packages/lutris/gui/installerwindow.py", line 155, in on_installer_selected
    self.interpreter = interpreter.ScriptInterpreter(
  File "/usr/lib/python3.10/site-packages/lutris/installer/interpreter.py", line 50, in __init__
    self.installer = LutrisInstaller(installer, self, service=self.service, appid=self.appid)
  File "/usr/lib/python3.10/site-packages/lutris/installer/installer.py", line 31, in __init__
    self.service = self.get_service(initial=service)
  File "/usr/lib/python3.10/site-packages/lutris/installer/installer.py", line 47, in get_service
    return SERVICES["steam"]()
KeyError: 'steam'


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `lutris/installer/installer.py`
Content:
```
1 """Lutris installer class"""
2 import json
3 import os
4 from gettext import gettext as _
5 
6 from lutris.config import LutrisConfig, write_game_config
7 from lutris.database.games import add_or_update, get_game_by_field
8 from lutris.game import Game
9 from lutris.installer import AUTO_ELF_EXE, AUTO_WIN32_EXE
10 from lutris.installer.errors import ScriptingError
11 from lutris.installer.installer_file import InstallerFile
12 from lutris.installer.legacy import get_game_launcher
13 from lutris.runners import import_runner
14 from lutris.services import SERVICES
15 from lutris.util.game_finder import find_linux_game_executable, find_windows_game_executable
16 from lutris.util.log import logger
17 
18 
19 class LutrisInstaller:  # pylint: disable=too-many-instance-attributes
20     """Represents a Lutris installer"""
21 
22     def __init__(self, installer, interpreter, service, appid):
23         self.interpreter = interpreter
24         self.installer = installer
25         self.version = installer["version"]
26         self.slug = installer["slug"]
27         self.year = installer.get("year")
28         self.runner = installer["runner"]
29         self.script = installer.get("script")
30         self.game_name = installer["name"]
31         self.game_slug = installer["game_slug"]
32         self.service = self.get_service(initial=service)
33         self.service_appid = self.get_appid(installer, initial=appid)
34         self.variables = installer.get("variables", {})
35         self.files = [
36             InstallerFile(self.game_slug, file_id, file_meta)
37             for file_desc in self.script.get("files", [])
38             for file_id, file_meta in file_desc.items()
39         ]
40         self.requires = self.script.get("requires")
41         self.extends = self.script.get("extends")
42         self.game_id = self.get_game_id()
43 
44     def get_service(self, initial=None):
45         if initial:
46             return initial
47         if "steam" in self.runner:
48             return SERVICES["steam"]()
49         version = self.version.lower()
50         if "humble" in version:
51             return SERVICES["humblebundle"]()
52         if "gog" in version:
53             return SERVICES["gog"]()
54 
55     def get_appid(self, installer, initial=None):
56         if initial:
57             return initial
58         if not self.service:
59             return
60         if self.service.id == "steam":
61             return installer.get("steamid")
62         game_config = self.script.get("game", {})
63         if self.service.id == "gog":
64             return game_config.get("gogid") or installer.get("gogid")
65         if self.service.id == "humblebundle":
66             return game_config.get("humbleid") or installer.get("humblestoreid")
67 
68     @property
69     def script_pretty(self):
70         """Return a pretty print of the script"""
71         return json.dumps(self.script, indent=4)
72 
73     def get_game_id(self):
74         """Return the ID of the game in the local DB if one exists"""
75         # If the game is in the library and uninstalled, the first installation
76         # updates it
77         existing_game = get_game_by_field(self.game_slug, "slug")
78         if existing_game and not existing_game["installed"]:
79             return existing_game["id"]
80 
81     @property
82     def creates_game_folder(self):
83         """Determines if an install script should create a game folder for the game"""
84         if self.requires:
85             # Game is an extension of an existing game, folder exists
86             return False
87         if self.runner == "steam":
88             # Steam games installs in their steamapps directory
89             return False
90         if (
91                 self.files
92                 or self.script.get("game", {}).get("gog")
93                 or self.script.get("game", {}).get("prefix")
94         ):
95             return True
96         command_names = [list(c.keys())[0] for c in self.script.get("installer", [])]
97         if "insert-disc" in command_names:
98             return True
99         return False
100 
101     def get_errors(self):
102         """Return potential errors in the script"""
103         errors = []
104         if not isinstance(self.script, dict):
105             errors.append("Script must be a dictionary")
106             # Return early since the method assumes a dict
107             return errors
108 
109         # Check that installers contains all required fields
110         for field in ("runner", "game_name", "game_slug"):
111             if not hasattr(self, field) or not getattr(self, field):
112                 errors.append("Missing field '%s'" % field)
113 
114         # Check that libretro installers have a core specified
115         if self.runner == "libretro":
116             if "game" not in self.script or "core" not in self.script["game"]:
117                 errors.append("Missing libretro core in game section")
118 
119         # Check that Steam games have an AppID
120         if self.runner == "steam":
121             if not self.script.get("game", {}).get("appid"):
122                 errors.append("Missing appid for Steam game")
123 
124         # Check that installers don't contain both 'requires' and 'extends'
125         if self.script.get("requires") and self.script.get("extends"):
126             errors.append("Scripts can't have both extends and requires")
127         return errors
128 
129     def pop_user_provided_file(self):
130         """Return and remove the first user provided file, which is used for game stores"""
131         for index, file in enumerate(self.files):
132             if file.url.startswith("N/A"):
133                 self.files.pop(index)
134                 return file.id
135 
136     def prepare_game_files(self):
137         """Gathers necessary files before iterating through them."""
138         if not self.files:
139             return
140         if self.service:
141             if self.service.online and not self.service.is_connected():
142                 logger.info("Not authenticated to %s", self.service.id)
143                 return
144             installer_file_id = self.pop_user_provided_file()
145             if not installer_file_id:
146                 logger.warning("Could not find a file for this service")
147                 return
148             if self.service.has_extras:
149                 self.service.selected_extras = self.interpreter.extras
150             installer_files = self.service.get_installer_files(self, installer_file_id)
151             for installer_file in installer_files:
152                 self.files.append(installer_file)
153             if not installer_files:
154                 # Failed to get the service game, put back a user provided file
155                 self.files.insert(0, "N/A: Provider installer file")
156 
157     def _substitute_config(self, script_config):
158         """Substitute values such as $GAMEDIR in a config dict."""
159         config = {}
160         for key in script_config:
161             if not isinstance(key, str):
162                 raise ScriptingError(_("Game config key must be a string"), key)
163             value = script_config[key]
164             if str(value).lower() == 'true':
165                 value = True
166             if str(value).lower() == 'false':
167                 value = False
168             if isinstance(value, list):
169                 config[key] = [self.interpreter._substitute(i) for i in value]
170             elif isinstance(value, dict):
171                 config[key] = {k: self.interpreter._substitute(v) for (k, v) in value.items()}
172             elif isinstance(value, bool):
173                 config[key] = value
174             else:
175                 config[key] = self.interpreter._substitute(value)
176         return config
177 
178     def get_game_config(self):
179         """Return the game configuration"""
180         if self.requires:
181             # Load the base game config
182             required_game = get_game_by_field(self.requires, field="installer_slug")
183             if not required_game:
184                 required_game = get_game_by_field(self.requires, field="slug")
185             if not required_game:
186                 raise ValueError("No game matched '%s' on installer_slug or slug" % self.requires)
187             base_config = LutrisConfig(
188                 runner_slug=self.runner, game_config_id=required_game["configpath"]
189             )
190             config = base_config.game_level
191         else:
192             config = {"game": {}}
193 
194         # Config update
195         if "system" in self.script:
196             config["system"] = self._substitute_config(self.script["system"])
197         if self.runner in self.script and self.script[self.runner]:
198             config[self.runner] = self._substitute_config(self.script[self.runner])
199         launcher, launcher_config = self.get_game_launcher_config(self.interpreter.game_files)
200         if launcher:
201             config["game"][launcher] = launcher_config
202 
203         if "game" in self.script:
204             try:
205                 config["game"].update(self.script["game"])
206             except ValueError as err:
207                 raise ScriptingError(_("Invalid 'game' section"), self.script["game"]) from err
208             config["game"] = self._substitute_config(config["game"])
209             if AUTO_ELF_EXE in config["game"].get("exe", ""):
210                 config["game"]["exe"] = find_linux_game_executable(self.interpreter.target_path,
211                                                                    make_executable=True)
212             elif AUTO_WIN32_EXE in config["game"].get("exe", ""):
213                 config["game"]["exe"] = find_windows_game_executable(self.interpreter.target_path)
214         return config
215 
216     def save(self):
217         """Write the game configuration in the DB and config file"""
218         if self.extends:
219             logger.info(
220                 "This is an extension to %s, not creating a new game entry",
221                 self.extends,
222             )
223             return
224         configpath = write_game_config(self.slug, self.get_game_config())
225         runner_inst = import_runner(self.runner)()
226         if self.service:
227             service_id = self.service.id
228         else:
229             service_id = None
230         self.game_id = add_or_update(
231             name=self.game_name,
232             runner=self.runner,
233             slug=self.game_slug,
234             platform=runner_inst.get_platform(),
235             directory=self.interpreter.target_path,
236             installed=1,
237             hidden=0,
238             installer_slug=self.slug,
239             parent_slug=self.requires,
240             year=self.year,
241             configpath=configpath,
242             service=service_id,
243             service_id=self.service_appid,
244             id=self.game_id,
245         )
246         # This is a bit redundant but used to trigger the game-updated signal
247         game = Game(self.game_id)
248         game.save()
249 
250     def get_game_launcher_config(self, game_files):
251         """Game options such as exe or main_file can be added at the root of the
252         script as a shortcut, this integrates them into the game config properly
253         This should be deprecated. Game launchers should go in the game section.
254         """
255         launcher, launcher_value = get_game_launcher(self.script)
256         if isinstance(launcher_value, list):
257             launcher_values = []
258             for game_file in launcher_value:
259                 if game_file in game_files:
260                     launcher_values.append(game_files[game_file])
261                 else:
262                     launcher_values.append(game_file)
263             return launcher, launcher_values
264         if launcher_value:
265             if launcher_value in game_files:
266                 launcher_value = game_files[launcher_value]
267             elif self.interpreter.target_path and os.path.exists(
268                     os.path.join(self.interpreter.target_path, launcher_value)
269             ):
270                 launcher_value = os.path.join(self.interpreter.target_path, launcher_value)
271         return launcher, launcher_value
272
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/lutris/installer/installer.py b/lutris/installer/installer.py
--- a/lutris/installer/installer.py
+++ b/lutris/installer/installer.py
@@ -44,12 +44,12 @@
     def get_service(self, initial=None):
         if initial:
             return initial
-        if "steam" in self.runner:
+        if "steam" in self.runner and "steam" in SERVICES:
             return SERVICES["steam"]()
         version = self.version.lower()
-        if "humble" in version:
+        if "humble" in version and "humblebundle" in SERVICES:
             return SERVICES["humblebundle"]()
-        if "gog" in version:
+        if "gog" in version and "gog" in SERVICES:
             return SERVICES["gog"]()
 
     def get_appid(self, installer, initial=None):

{"golden_diff": "diff --git a/lutris/installer/installer.py b/lutris/installer/installer.py\n--- a/lutris/installer/installer.py\n+++ b/lutris/installer/installer.py\n@@ -44,12 +44,12 @@\n     def get_service(self, initial=None):\n         if initial:\n             return initial\n-        if \"steam\" in self.runner:\n+        if \"steam\" in self.runner and \"steam\" in SERVICES:\n             return SERVICES[\"steam\"]()\n         version = self.version.lower()\n-        if \"humble\" in version:\n+        if \"humble\" in version and \"humblebundle\" in SERVICES:\n             return SERVICES[\"humblebundle\"]()\n-        if \"gog\" in version:\n+        if \"gog\" in version and \"gog\" in SERVICES:\n             return SERVICES[\"gog\"]()\n \n     def get_appid(self, installer, initial=None):\n", "issue": "Installer didnt Start Errors in .py\ni want to install some stuff like kotor with steam but get everytime the same error.\r\n\r\nFile \"/usr/lib/python3.10/site-packages/lutris/gui/installerwindow.py\", line 155, in on_installer_selected\r\n    self.interpreter = interpreter.ScriptInterpreter(\r\n  File \"/usr/lib/python3.10/site-packages/lutris/installer/interpreter.py\", line 50, in __init__\r\n    self.installer = LutrisInstaller(installer, self, service=self.service, appid=self.appid)\r\n  File \"/usr/lib/python3.10/site-packages/lutris/installer/installer.py\", line 31, in __init__\r\n    self.service = self.get_service(initial=service)\r\n  File \"/usr/lib/python3.10/site-packages/lutris/installer/installer.py\", line 47, in get_service\r\n    return SERVICES[\"steam\"]()\r\nKeyError: 'steam'\r\n\n", "before_files": [{"content": "\"\"\"Lutris installer class\"\"\"\nimport json\nimport os\nfrom gettext import gettext as _\n\nfrom lutris.config import LutrisConfig, write_game_config\nfrom lutris.database.games import add_or_update, get_game_by_field\nfrom lutris.game import Game\nfrom lutris.installer import AUTO_ELF_EXE, AUTO_WIN32_EXE\nfrom lutris.installer.errors import ScriptingError\nfrom lutris.installer.installer_file import InstallerFile\nfrom lutris.installer.legacy import get_game_launcher\nfrom lutris.runners import import_runner\nfrom lutris.services import SERVICES\nfrom lutris.util.game_finder import find_linux_game_executable, find_windows_game_executable\nfrom lutris.util.log import logger\n\n\nclass LutrisInstaller:  # pylint: disable=too-many-instance-attributes\n    \"\"\"Represents a Lutris installer\"\"\"\n\n    def __init__(self, installer, interpreter, service, appid):\n        self.interpreter = interpreter\n        self.installer = installer\n        self.version = installer[\"version\"]\n        self.slug = installer[\"slug\"]\n        self.year = installer.get(\"year\")\n        self.runner = installer[\"runner\"]\n        self.script = installer.get(\"script\")\n        self.game_name = installer[\"name\"]\n        self.game_slug = installer[\"game_slug\"]\n        self.service = self.get_service(initial=service)\n        self.service_appid = self.get_appid(installer, initial=appid)\n        self.variables = installer.get(\"variables\", {})\n        self.files = [\n            InstallerFile(self.game_slug, file_id, file_meta)\n            for file_desc in self.script.get(\"files\", [])\n            for file_id, file_meta in file_desc.items()\n        ]\n        self.requires = self.script.get(\"requires\")\n        self.extends = self.script.get(\"extends\")\n        self.game_id = self.get_game_id()\n\n    def get_service(self, initial=None):\n        if initial:\n            return initial\n        if \"steam\" in self.runner:\n            return SERVICES[\"steam\"]()\n        version = self.version.lower()\n        if \"humble\" in version:\n            return SERVICES[\"humblebundle\"]()\n        if \"gog\" in version:\n            return SERVICES[\"gog\"]()\n\n    def get_appid(self, installer, initial=None):\n        if initial:\n            return initial\n        if not self.service:\n            return\n        if self.service.id == \"steam\":\n            return installer.get(\"steamid\")\n        game_config = self.script.get(\"game\", {})\n        if self.service.id == \"gog\":\n            return game_config.get(\"gogid\") or installer.get(\"gogid\")\n        if self.service.id == \"humblebundle\":\n            return game_config.get(\"humbleid\") or installer.get(\"humblestoreid\")\n\n    @property\n    def script_pretty(self):\n        \"\"\"Return a pretty print of the script\"\"\"\n        return json.dumps(self.script, indent=4)\n\n    def get_game_id(self):\n        \"\"\"Return the ID of the game in the local DB if one exists\"\"\"\n        # If the game is in the library and uninstalled, the first installation\n        # updates it\n        existing_game = get_game_by_field(self.game_slug, \"slug\")\n        if existing_game and not existing_game[\"installed\"]:\n            return existing_game[\"id\"]\n\n    @property\n    def creates_game_folder(self):\n        \"\"\"Determines if an install script should create a game folder for the game\"\"\"\n        if self.requires:\n            # Game is an extension of an existing game, folder exists\n            return False\n        if self.runner == \"steam\":\n            # Steam games installs in their steamapps directory\n            return False\n        if (\n                self.files\n                or self.script.get(\"game\", {}).get(\"gog\")\n                or self.script.get(\"game\", {}).get(\"prefix\")\n        ):\n            return True\n        command_names = [list(c.keys())[0] for c in self.script.get(\"installer\", [])]\n        if \"insert-disc\" in command_names:\n            return True\n        return False\n\n    def get_errors(self):\n        \"\"\"Return potential errors in the script\"\"\"\n        errors = []\n        if not isinstance(self.script, dict):\n            errors.append(\"Script must be a dictionary\")\n            # Return early since the method assumes a dict\n            return errors\n\n        # Check that installers contains all required fields\n        for field in (\"runner\", \"game_name\", \"game_slug\"):\n            if not hasattr(self, field) or not getattr(self, field):\n                errors.append(\"Missing field '%s'\" % field)\n\n        # Check that libretro installers have a core specified\n        if self.runner == \"libretro\":\n            if \"game\" not in self.script or \"core\" not in self.script[\"game\"]:\n                errors.append(\"Missing libretro core in game section\")\n\n        # Check that Steam games have an AppID\n        if self.runner == \"steam\":\n            if not self.script.get(\"game\", {}).get(\"appid\"):\n                errors.append(\"Missing appid for Steam game\")\n\n        # Check that installers don't contain both 'requires' and 'extends'\n        if self.script.get(\"requires\") and self.script.get(\"extends\"):\n            errors.append(\"Scripts can't have both extends and requires\")\n        return errors\n\n    def pop_user_provided_file(self):\n        \"\"\"Return and remove the first user provided file, which is used for game stores\"\"\"\n        for index, file in enumerate(self.files):\n            if file.url.startswith(\"N/A\"):\n                self.files.pop(index)\n                return file.id\n\n    def prepare_game_files(self):\n        \"\"\"Gathers necessary files before iterating through them.\"\"\"\n        if not self.files:\n            return\n        if self.service:\n            if self.service.online and not self.service.is_connected():\n                logger.info(\"Not authenticated to %s\", self.service.id)\n                return\n            installer_file_id = self.pop_user_provided_file()\n            if not installer_file_id:\n                logger.warning(\"Could not find a file for this service\")\n                return\n            if self.service.has_extras:\n                self.service.selected_extras = self.interpreter.extras\n            installer_files = self.service.get_installer_files(self, installer_file_id)\n            for installer_file in installer_files:\n                self.files.append(installer_file)\n            if not installer_files:\n                # Failed to get the service game, put back a user provided file\n                self.files.insert(0, \"N/A: Provider installer file\")\n\n    def _substitute_config(self, script_config):\n        \"\"\"Substitute values such as $GAMEDIR in a config dict.\"\"\"\n        config = {}\n        for key in script_config:\n            if not isinstance(key, str):\n                raise ScriptingError(_(\"Game config key must be a string\"), key)\n            value = script_config[key]\n            if str(value).lower() == 'true':\n                value = True\n            if str(value).lower() == 'false':\n                value = False\n            if isinstance(value, list):\n                config[key] = [self.interpreter._substitute(i) for i in value]\n            elif isinstance(value, dict):\n                config[key] = {k: self.interpreter._substitute(v) for (k, v) in value.items()}\n            elif isinstance(value, bool):\n                config[key] = value\n            else:\n                config[key] = self.interpreter._substitute(value)\n        return config\n\n    def get_game_config(self):\n        \"\"\"Return the game configuration\"\"\"\n        if self.requires:\n            # Load the base game config\n            required_game = get_game_by_field(self.requires, field=\"installer_slug\")\n            if not required_game:\n                required_game = get_game_by_field(self.requires, field=\"slug\")\n            if not required_game:\n                raise ValueError(\"No game matched '%s' on installer_slug or slug\" % self.requires)\n            base_config = LutrisConfig(\n                runner_slug=self.runner, game_config_id=required_game[\"configpath\"]\n            )\n            config = base_config.game_level\n        else:\n            config = {\"game\": {}}\n\n        # Config update\n        if \"system\" in self.script:\n            config[\"system\"] = self._substitute_config(self.script[\"system\"])\n        if self.runner in self.script and self.script[self.runner]:\n            config[self.runner] = self._substitute_config(self.script[self.runner])\n        launcher, launcher_config = self.get_game_launcher_config(self.interpreter.game_files)\n        if launcher:\n            config[\"game\"][launcher] = launcher_config\n\n        if \"game\" in self.script:\n            try:\n                config[\"game\"].update(self.script[\"game\"])\n            except ValueError as err:\n                raise ScriptingError(_(\"Invalid 'game' section\"), self.script[\"game\"]) from err\n            config[\"game\"] = self._substitute_config(config[\"game\"])\n            if AUTO_ELF_EXE in config[\"game\"].get(\"exe\", \"\"):\n                config[\"game\"][\"exe\"] = find_linux_game_executable(self.interpreter.target_path,\n                                                                   make_executable=True)\n            elif AUTO_WIN32_EXE in config[\"game\"].get(\"exe\", \"\"):\n                config[\"game\"][\"exe\"] = find_windows_game_executable(self.interpreter.target_path)\n        return config\n\n    def save(self):\n        \"\"\"Write the game configuration in the DB and config file\"\"\"\n        if self.extends:\n            logger.info(\n                \"This is an extension to %s, not creating a new game entry\",\n                self.extends,\n            )\n            return\n        configpath = write_game_config(self.slug, self.get_game_config())\n        runner_inst = import_runner(self.runner)()\n        if self.service:\n            service_id = self.service.id\n        else:\n            service_id = None\n        self.game_id = add_or_update(\n            name=self.game_name,\n            runner=self.runner,\n            slug=self.game_slug,\n            platform=runner_inst.get_platform(),\n            directory=self.interpreter.target_path,\n            installed=1,\n            hidden=0,\n            installer_slug=self.slug,\n            parent_slug=self.requires,\n            year=self.year,\n            configpath=configpath,\n            service=service_id,\n            service_id=self.service_appid,\n            id=self.game_id,\n        )\n        # This is a bit redundant but used to trigger the game-updated signal\n        game = Game(self.game_id)\n        game.save()\n\n    def get_game_launcher_config(self, game_files):\n        \"\"\"Game options such as exe or main_file can be added at the root of the\n        script as a shortcut, this integrates them into the game config properly\n        This should be deprecated. Game launchers should go in the game section.\n        \"\"\"\n        launcher, launcher_value = get_game_launcher(self.script)\n        if isinstance(launcher_value, list):\n            launcher_values = []\n            for game_file in launcher_value:\n                if game_file in game_files:\n                    launcher_values.append(game_files[game_file])\n                else:\n                    launcher_values.append(game_file)\n            return launcher, launcher_values\n        if launcher_value:\n            if launcher_value in game_files:\n                launcher_value = game_files[launcher_value]\n            elif self.interpreter.target_path and os.path.exists(\n                    os.path.join(self.interpreter.target_path, launcher_value)\n            ):\n                launcher_value = os.path.join(self.interpreter.target_path, launcher_value)\n        return launcher, launcher_value\n", "path": "lutris/installer/installer.py"}], "after_files": [{"content": "\"\"\"Lutris installer class\"\"\"\nimport json\nimport os\nfrom gettext import gettext as _\n\nfrom lutris.config import LutrisConfig, write_game_config\nfrom lutris.database.games import add_or_update, get_game_by_field\nfrom lutris.game import Game\nfrom lutris.installer import AUTO_ELF_EXE, AUTO_WIN32_EXE\nfrom lutris.installer.errors import ScriptingError\nfrom lutris.installer.installer_file import InstallerFile\nfrom lutris.installer.legacy import get_game_launcher\nfrom lutris.runners import import_runner\nfrom lutris.services import SERVICES\nfrom lutris.util.game_finder import find_linux_game_executable, find_windows_game_executable\nfrom lutris.util.log import logger\n\n\nclass LutrisInstaller:  # pylint: disable=too-many-instance-attributes\n    \"\"\"Represents a Lutris installer\"\"\"\n\n    def __init__(self, installer, interpreter, service, appid):\n        self.interpreter = interpreter\n        self.installer = installer\n        self.version = installer[\"version\"]\n        self.slug = installer[\"slug\"]\n        self.year = installer.get(\"year\")\n        self.runner = installer[\"runner\"]\n        self.script = installer.get(\"script\")\n        self.game_name = installer[\"name\"]\n        self.game_slug = installer[\"game_slug\"]\n        self.service = self.get_service(initial=service)\n        self.service_appid = self.get_appid(installer, initial=appid)\n        self.variables = installer.get(\"variables\", {})\n        self.files = [\n            InstallerFile(self.game_slug, file_id, file_meta)\n            for file_desc in self.script.get(\"files\", [])\n            for file_id, file_meta in file_desc.items()\n        ]\n        self.requires = self.script.get(\"requires\")\n        self.extends = self.script.get(\"extends\")\n        self.game_id = self.get_game_id()\n\n    def get_service(self, initial=None):\n        if initial:\n            return initial\n        if \"steam\" in self.runner and \"steam\" in SERVICES:\n            return SERVICES[\"steam\"]()\n        version = self.version.lower()\n        if \"humble\" in version and \"humblebundle\" in SERVICES:\n            return SERVICES[\"humblebundle\"]()\n        if \"gog\" in version and \"gog\" in SERVICES:\n            return SERVICES[\"gog\"]()\n\n    def get_appid(self, installer, initial=None):\n        if initial:\n            return initial\n        if not self.service:\n            return\n        if self.service.id == \"steam\":\n            return installer.get(\"steamid\")\n        game_config = self.script.get(\"game\", {})\n        if self.service.id == \"gog\":\n            return game_config.get(\"gogid\") or installer.get(\"gogid\")\n        if self.service.id == \"humblebundle\":\n            return game_config.get(\"humbleid\") or installer.get(\"humblestoreid\")\n\n    @property\n    def script_pretty(self):\n        \"\"\"Return a pretty print of the script\"\"\"\n        return json.dumps(self.script, indent=4)\n\n    def get_game_id(self):\n        \"\"\"Return the ID of the game in the local DB if one exists\"\"\"\n        # If the game is in the library and uninstalled, the first installation\n        # updates it\n        existing_game = get_game_by_field(self.game_slug, \"slug\")\n        if existing_game and not existing_game[\"installed\"]:\n            return existing_game[\"id\"]\n\n    @property\n    def creates_game_folder(self):\n        \"\"\"Determines if an install script should create a game folder for the game\"\"\"\n        if self.requires:\n            # Game is an extension of an existing game, folder exists\n            return False\n        if self.runner == \"steam\":\n            # Steam games installs in their steamapps directory\n            return False\n        if (\n                self.files\n                or self.script.get(\"game\", {}).get(\"gog\")\n                or self.script.get(\"game\", {}).get(\"prefix\")\n        ):\n            return True\n        command_names = [list(c.keys())[0] for c in self.script.get(\"installer\", [])]\n        if \"insert-disc\" in command_names:\n            return True\n        return False\n\n    def get_errors(self):\n        \"\"\"Return potential errors in the script\"\"\"\n        errors = []\n        if not isinstance(self.script, dict):\n            errors.append(\"Script must be a dictionary\")\n            # Return early since the method assumes a dict\n            return errors\n\n        # Check that installers contains all required fields\n        for field in (\"runner\", \"game_name\", \"game_slug\"):\n            if not hasattr(self, field) or not getattr(self, field):\n                errors.append(\"Missing field '%s'\" % field)\n\n        # Check that libretro installers have a core specified\n        if self.runner == \"libretro\":\n            if \"game\" not in self.script or \"core\" not in self.script[\"game\"]:\n                errors.append(\"Missing libretro core in game section\")\n\n        # Check that Steam games have an AppID\n        if self.runner == \"steam\":\n            if not self.script.get(\"game\", {}).get(\"appid\"):\n                errors.append(\"Missing appid for Steam game\")\n\n        # Check that installers don't contain both 'requires' and 'extends'\n        if self.script.get(\"requires\") and self.script.get(\"extends\"):\n            errors.append(\"Scripts can't have both extends and requires\")\n        return errors\n\n    def pop_user_provided_file(self):\n        \"\"\"Return and remove the first user provided file, which is used for game stores\"\"\"\n        for index, file in enumerate(self.files):\n            if file.url.startswith(\"N/A\"):\n                self.files.pop(index)\n                return file.id\n\n    def prepare_game_files(self):\n        \"\"\"Gathers necessary files before iterating through them.\"\"\"\n        if not self.files:\n            return\n        if self.service:\n            if self.service.online and not self.service.is_connected():\n                logger.info(\"Not authenticated to %s\", self.service.id)\n                return\n            installer_file_id = self.pop_user_provided_file()\n            if not installer_file_id:\n                logger.warning(\"Could not find a file for this service\")\n                return\n            if self.service.has_extras:\n                self.service.selected_extras = self.interpreter.extras\n            installer_files = self.service.get_installer_files(self, installer_file_id)\n            for installer_file in installer_files:\n                self.files.append(installer_file)\n            if not installer_files:\n                # Failed to get the service game, put back a user provided file\n                self.files.insert(0, \"N/A: Provider installer file\")\n\n    def _substitute_config(self, script_config):\n        \"\"\"Substitute values such as $GAMEDIR in a config dict.\"\"\"\n        config = {}\n        for key in script_config:\n            if not isinstance(key, str):\n                raise ScriptingError(_(\"Game config key must be a string\"), key)\n            value = script_config[key]\n            if str(value).lower() == 'true':\n                value = True\n            if str(value).lower() == 'false':\n                value = False\n            if isinstance(value, list):\n                config[key] = [self.interpreter._substitute(i) for i in value]\n            elif isinstance(value, dict):\n                config[key] = {k: self.interpreter._substitute(v) for (k, v) in value.items()}\n            elif isinstance(value, bool):\n                config[key] = value\n            else:\n                config[key] = self.interpreter._substitute(value)\n        return config\n\n    def get_game_config(self):\n        \"\"\"Return the game configuration\"\"\"\n        if self.requires:\n            # Load the base game config\n            required_game = get_game_by_field(self.requires, field=\"installer_slug\")\n            if not required_game:\n                required_game = get_game_by_field(self.requires, field=\"slug\")\n            if not required_game:\n                raise ValueError(\"No game matched '%s' on installer_slug or slug\" % self.requires)\n            base_config = LutrisConfig(\n                runner_slug=self.runner, game_config_id=required_game[\"configpath\"]\n            )\n            config = base_config.game_level\n        else:\n            config = {\"game\": {}}\n\n        # Config update\n        if \"system\" in self.script:\n            config[\"system\"] = self._substitute_config(self.script[\"system\"])\n        if self.runner in self.script and self.script[self.runner]:\n            config[self.runner] = self._substitute_config(self.script[self.runner])\n        launcher, launcher_config = self.get_game_launcher_config(self.interpreter.game_files)\n        if launcher:\n            config[\"game\"][launcher] = launcher_config\n\n        if \"game\" in self.script:\n            try:\n                config[\"game\"].update(self.script[\"game\"])\n            except ValueError as err:\n                raise ScriptingError(_(\"Invalid 'game' section\"), self.script[\"game\"]) from err\n            config[\"game\"] = self._substitute_config(config[\"game\"])\n            if AUTO_ELF_EXE in config[\"game\"].get(\"exe\", \"\"):\n                config[\"game\"][\"exe\"] = find_linux_game_executable(self.interpreter.target_path,\n                                                                   make_executable=True)\n            elif AUTO_WIN32_EXE in config[\"game\"].get(\"exe\", \"\"):\n                config[\"game\"][\"exe\"] = find_windows_game_executable(self.interpreter.target_path)\n        return config\n\n    def save(self):\n        \"\"\"Write the game configuration in the DB and config file\"\"\"\n        if self.extends:\n            logger.info(\n                \"This is an extension to %s, not creating a new game entry\",\n                self.extends,\n            )\n            return\n        configpath = write_game_config(self.slug, self.get_game_config())\n        runner_inst = import_runner(self.runner)()\n        if self.service:\n            service_id = self.service.id\n        else:\n            service_id = None\n        self.game_id = add_or_update(\n            name=self.game_name,\n            runner=self.runner,\n            slug=self.game_slug,\n            platform=runner_inst.get_platform(),\n            directory=self.interpreter.target_path,\n            installed=1,\n            hidden=0,\n            installer_slug=self.slug,\n            parent_slug=self.requires,\n            year=self.year,\n            configpath=configpath,\n            service=service_id,\n            service_id=self.service_appid,\n            id=self.game_id,\n        )\n        # This is a bit redundant but used to trigger the game-updated signal\n        game = Game(self.game_id)\n        game.save()\n\n    def get_game_launcher_config(self, game_files):\n        \"\"\"Game options such as exe or main_file can be added at the root of the\n        script as a shortcut, this integrates them into the game config properly\n        This should be deprecated. Game launchers should go in the game section.\n        \"\"\"\n        launcher, launcher_value = get_game_launcher(self.script)\n        if isinstance(launcher_value, list):\n            launcher_values = []\n            for game_file in launcher_value:\n                if game_file in game_files:\n                    launcher_values.append(game_files[game_file])\n                else:\n                    launcher_values.append(game_file)\n            return launcher, launcher_values\n        if launcher_value:\n            if launcher_value in game_files:\n                launcher_value = game_files[launcher_value]\n            elif self.interpreter.target_path and os.path.exists(\n                    os.path.join(self.interpreter.target_path, launcher_value)\n            ):\n                launcher_value = os.path.join(self.interpreter.target_path, launcher_value)\n        return launcher, launcher_value\n", "path": "lutris/installer/installer.py"}]}