Skip to content

pre_workfile_preparation

Hook to launch Unreal and prepare projects.

UnrealPrelaunchHook

Bases: PreLaunchHook

Hook to handle launching Unreal.

This hook will check if current workfile path has Unreal project inside. IF not, it initializes it, and finally it pass path to the project by environment variable to Unreal launcher shell script.

Source code in client/ayon_unreal/hooks/pre_workfile_preparation.py
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
class UnrealPrelaunchHook(PreLaunchHook):
    """Hook to handle launching Unreal.

    This hook will check if current workfile path has Unreal
    project inside. IF not, it initializes it, and finally it pass
    path to the project by environment variable to Unreal launcher
    shell script.

    """
    app_groups = {"unreal"}
    launch_types = {LaunchTypes.local}

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.signature = f"( {self.__class__.__name__} )"

    def _get_work_filename(self):
        # Use last workfile if was found
        if self.data.get("last_workfile_path"):
            last_workfile = Path(self.data.get("last_workfile_path"))
            if last_workfile and last_workfile.exists():
                return last_workfile.name

        # Prepare data for fill data and for getting workfile template key
        anatomy = self.data["anatomy"]
        project_entity = self.data["project_entity"]

        # Use already prepared workdir data
        workdir_data = copy.deepcopy(self.data["workdir_data"])
        task_type = workdir_data.get("task", {}).get("type")

        # QUESTION raise exception if version is part of filename template?
        workdir_data["version"] = 1
        workdir_data["ext"] = "uproject"

        # Get workfile template key for current context
        workfile_template_key = get_workfile_template_key(
            project_entity["name"],
            task_type,
            self.host_name,
        )
        # Fill templates
        template_obj = anatomy.get_template_item(
            "work", workfile_template_key, "file"
        )

        # Return filename
        return template_obj.format_strict(workdir_data)

    def exec_plugin_install(self, engine_path: Path, env: dict = None):
        # set up the QThread and worker with necessary signals
        env = env or os.environ
        if not QtWidgets.QApplication.instance():
            QtWidgets.QApplication(sys.argv)
        q_thread = QtCore.QThread()
        ue_plugin_worker = UEPluginInstallWorker()

        q_thread.started.connect(ue_plugin_worker.run)
        ue_plugin_worker.setup(engine_path, env)
        ue_plugin_worker.moveToThread(q_thread)

        splash_screen = SplashScreen(
            "Installing plugin",
            resources.get_resource("app_icons", "ue4.png")
        )

        # set up the splash screen with necessary triggers
        ue_plugin_worker.installing.connect(
            splash_screen.update_top_label_text
        )
        ue_plugin_worker.progress.connect(splash_screen.update_progress)
        ue_plugin_worker.log.connect(splash_screen.append_log)
        ue_plugin_worker.finished.connect(splash_screen.quit_and_close)
        ue_plugin_worker.failed.connect(splash_screen.fail)

        splash_screen.start_thread(q_thread)
        splash_screen.show_ui()

        if not splash_screen.was_proc_successful():
            raise ApplicationLaunchFailed("Couldn't run the application! "
                                          "Plugin failed to install!")

    def exec_ue_project_gen(self,
                            engine_version: str,
                            unreal_project_name: str,
                            engine_path: Path,
                            project_dir: Path):
        self.log.info((
            f"{self.signature} Creating unreal "
            f"project [ {unreal_project_name} ]"
        ))
        if not QtWidgets.QApplication.instance():
            QtWidgets.QApplication(sys.argv)

        q_thread = QtCore.QThread()
        ue_project_worker = UEProjectGenerationWorker()
        ue_project_worker.setup(
            engine_version,
            self.data["project_name"],
            unreal_project_name,
            engine_path,
            project_dir
        )
        ue_project_worker.moveToThread(q_thread)
        q_thread.started.connect(ue_project_worker.run)

        splash_screen = SplashScreen(
            "Initializing UE project",
            resources.get_resource("app_icons", "ue4.png")
        )

        ue_project_worker.stage_begin.connect(
            splash_screen.update_top_label_text
        )
        ue_project_worker.progress.connect(splash_screen.update_progress)
        ue_project_worker.log.connect(splash_screen.append_log)
        ue_project_worker.finished.connect(splash_screen.quit_and_close)
        ue_project_worker.failed.connect(splash_screen.fail)

        splash_screen.start_thread(q_thread)
        splash_screen.show_ui()

        if not splash_screen.was_proc_successful():
            raise ApplicationLaunchFailed("Couldn't run the application! "
                                          "Failed to generate the project!")

    def execute(self):
        """Hook entry method."""
        workdir = self.launch_context.env["AYON_WORKDIR"]
        executable = str(self.launch_context.executable)
        engine_version = self.app_name.split("/")[-1].replace("-", ".")
        try:
            if int(engine_version.split(".")[0]) < 4 and \
                        int(engine_version.split(".")[1]) < 26:
                raise ApplicationLaunchFailed((
                    f"{self.signature} Old unsupported version of UE "
                    f"detected - {engine_version}"))
        except ValueError:
            # there can be string in minor version and in that case
            # int cast is failing. This probably happens only with
            # early access versions and is of no concert for this check
            # so let's keep it quiet.
            ...

        unreal_project_filename = self._get_work_filename()
        unreal_project_name = os.path.splitext(unreal_project_filename)[0]
        # Unreal is sensitive about project names longer then 20 chars
        if len(unreal_project_name) > 20:
            raise ApplicationLaunchFailed(
                f"Project name exceeds 20 characters ({unreal_project_name})!"
            )

        # Unreal doesn't accept non alphabet characters at the start
        # of the project name. This is because project name is then used
        # in various places inside c++ code and there variable names cannot
        # start with non-alpha. We append 'P' before project name to solve it.
        # 😱
        if not unreal_project_name[:1].isalpha():
            self.log.warning((
                "Project name doesn't start with alphabet "
                f"character ({unreal_project_name}). Appending 'P'"
            ))
            unreal_project_name = f"P{unreal_project_name}"
            unreal_project_filename = f'{unreal_project_name}.uproject'

        last_workfile_path = self.data.get("last_workfile_path")
        if last_workfile_path and os.path.exists(last_workfile_path):
            project_path = Path(os.path.dirname(last_workfile_path))
            unreal_project_filename = Path(os.path.basename(last_workfile_path))
        else:
            project_path = Path(os.path.join(workdir, unreal_project_name))
            project_path.mkdir(parents=True, exist_ok=True)

        self.log.info((
            f"{self.signature} requested UE version: "
            f"[ {engine_version} ]"
        ))

        # engine_path points to the specific Unreal Engine root
        # so, we are going up from the executable itself 3 levels.
        # on macOS it's 6 levels up as the executable lives under
        # ./UnrealEditor.app/Contents/MacOS/UnrealEditor
        if platform.system().lower() == "darwin":
            engine_path: Path = Path(executable).parents[6]
        else:
            engine_path: Path = Path(executable).parents[3]

        # Check if new env variable exists, and if it does, if the path
        # actually contains the plugin. If not, install it.

        built_plugin_path = self.launch_context.env.get(
            "AYON_BUILT_UNREAL_PLUGIN", None)

        if unreal_lib.check_built_plugin_existance(built_plugin_path):
            self.log.info((
                f"{self.signature} using existing built Ayon plugin from "
                f"{built_plugin_path}"
            ))
            unreal_lib.copy_built_plugin(engine_path, Path(built_plugin_path))
        else:
            # Set "AYON_UNREAL_PLUGIN" to current process environment for
            # execution of `create_unreal_project`
            env_key = "AYON_UNREAL_PLUGIN"
            if self.launch_context.env.get(env_key):
                self.log.info((
                    f"{self.signature} using Ayon plugin from "
                    f"{self.launch_context.env.get(env_key)}"
                ))
            if self.launch_context.env.get(env_key):
                os.environ[env_key] = self.launch_context.env[env_key]

            if not unreal_lib.check_plugin_existence(engine_path):
                self.exec_plugin_install(engine_path)

        project_file = project_path / unreal_project_filename

        self.launch_context.env["AYON_UNREAL_VERSION"] = engine_version

        if not project_file.is_file():

            # Get project settings -> allow project creation
            current_project = get_current_project_name()
            unreal_settings = get_project_settings(current_project).get("unreal")
            allow_project_creation = unreal_settings["project_setup"].get(
            "allow_project_creation")
            # add the project template options
            # add the custom path for the existing project
            if allow_project_creation:
                existing_uproject_directory = Path(
                    unreal_settings["project_setup"].get(
                        "existing_uproject_directory")
                )
                uproject_files = list(existing_uproject_directory.glob("*.uproject"))
                if (
                    existing_uproject_directory.exists() and
                    uproject_files
                ):
                    self.copy_project(existing_uproject_directory, project_path)
                    # rename the project folder copied from existing_uproject directory
                    new_project_path = project_path.parent / unreal_project_name
                    project_path.rename(new_project_path)

                    # find the copied uproject file in the new project directory
                    copied_uproject_files = list(new_project_path.glob("*.uproject"))
                    if len(copied_uproject_files) != 1:
                        raise ApplicationLaunchFailed(
                            f"{self.signature} Expected exactly one .uproject file in "
                            f"{new_project_path}, but found {len(copied_uproject_files)}. "
                            "Please check the project directory."
                        )
                    copied_uproject_file = copied_uproject_files[0]
                    # set the correct engine version on the copied file
                    self.set_engine_version(copied_uproject_file, engine_version)

                    # rename the copied uproject file to match the expected filename
                    copied_uproject_file.rename(new_project_path / unreal_project_filename)
                    self.log.info((
                        f"{self.signature} Renamed {copied_uproject_file.name} to "
                        f"{unreal_project_filename}"
                    ))
                else:
                    with tempfile.TemporaryDirectory() as temp_dir:
                        self.exec_ue_project_gen(engine_version,
                                                 unreal_project_name,
                                                 engine_path,
                                                 Path(temp_dir))
                        self.copy_project(Path(temp_dir), project_path)

            # if the template path has been found with unreal project
            # copy that existing project to ayon work directory
            elif unreal_settings["project_setup"].get(
                    "force_existing_project"):
                msg = (
                    "Could not open project; Project file not found.\n\n"
                    f"'{project_path.as_posix()}' \n\n"
                    "Please contact administrator.\n"
                    "Make sure the project is in the correct folder. "
                    "Or enable 'allow project creation' in studio "
                    "settings."
                )
                raise ApplicationLaunchFailed(msg)
            else:
                return

        # Append the project file to launch arguments
        self.launch_context.launch_args.append(
            f"\"{project_file.as_posix()}\"")

    def set_engine_version(self, uproject_path: Path, new_version: str):
        """Set the engine version in a Unreal project file.

        Args:
            uproject_path (Path): The path to the .uproject file.
            new_version (str): The new engine version to set.

        Raises:
            FileNotFoundError: If the .uproject file does not exist.
        """
        if not uproject_path.is_file():
            raise FileNotFoundError(f"File not found: {uproject_path}")

        try:
            data = json.loads(uproject_path.read_text(encoding="utf-8"))

        except json.JSONDecodeError as e:
            raise ApplicationLaunchFailed(
                f"{self.signature} Malformed .uproject file at {uproject_path}: {e}"
            ) from e

        # Set the new engine version
        data["EngineAssociation"] = new_version

        uproject_path.write_text(json.dumps(data, indent=4), encoding="utf-8")

        self.log.info(
            f"Engine version set to '{new_version}' for {uproject_path}"
        )

    def copy_project(self, source: Path, destination: Path):
        """Copy an Unreal project directory.

        Args:
            source (Path): The source project directory.
            destination (Path): The destination directory.
        """
        try:
            self.log.info((
                f"Moving from {source.as_posix()} to "
                f"{destination.as_posix()}"
            ))
            shutil.copytree(
                source, destination, dirs_exist_ok=True)

        except shutil.Error as e:
            msg = (
                f"{self.signature} Cannot copy directory {source.as_posix()} "
                f"to {destination.as_posix()} - {e}"
            )
            raise ApplicationLaunchFailed(msg) from e

copy_project(source, destination)

Copy an Unreal project directory.

Parameters:

Name Type Description Default
source Path

The source project directory.

required
destination Path

The destination directory.

required
Source code in client/ayon_unreal/hooks/pre_workfile_preparation.py
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
def copy_project(self, source: Path, destination: Path):
    """Copy an Unreal project directory.

    Args:
        source (Path): The source project directory.
        destination (Path): The destination directory.
    """
    try:
        self.log.info((
            f"Moving from {source.as_posix()} to "
            f"{destination.as_posix()}"
        ))
        shutil.copytree(
            source, destination, dirs_exist_ok=True)

    except shutil.Error as e:
        msg = (
            f"{self.signature} Cannot copy directory {source.as_posix()} "
            f"to {destination.as_posix()} - {e}"
        )
        raise ApplicationLaunchFailed(msg) from e

execute()

Hook entry method.

Source code in client/ayon_unreal/hooks/pre_workfile_preparation.py
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
def execute(self):
    """Hook entry method."""
    workdir = self.launch_context.env["AYON_WORKDIR"]
    executable = str(self.launch_context.executable)
    engine_version = self.app_name.split("/")[-1].replace("-", ".")
    try:
        if int(engine_version.split(".")[0]) < 4 and \
                    int(engine_version.split(".")[1]) < 26:
            raise ApplicationLaunchFailed((
                f"{self.signature} Old unsupported version of UE "
                f"detected - {engine_version}"))
    except ValueError:
        # there can be string in minor version and in that case
        # int cast is failing. This probably happens only with
        # early access versions and is of no concert for this check
        # so let's keep it quiet.
        ...

    unreal_project_filename = self._get_work_filename()
    unreal_project_name = os.path.splitext(unreal_project_filename)[0]
    # Unreal is sensitive about project names longer then 20 chars
    if len(unreal_project_name) > 20:
        raise ApplicationLaunchFailed(
            f"Project name exceeds 20 characters ({unreal_project_name})!"
        )

    # Unreal doesn't accept non alphabet characters at the start
    # of the project name. This is because project name is then used
    # in various places inside c++ code and there variable names cannot
    # start with non-alpha. We append 'P' before project name to solve it.
    # 😱
    if not unreal_project_name[:1].isalpha():
        self.log.warning((
            "Project name doesn't start with alphabet "
            f"character ({unreal_project_name}). Appending 'P'"
        ))
        unreal_project_name = f"P{unreal_project_name}"
        unreal_project_filename = f'{unreal_project_name}.uproject'

    last_workfile_path = self.data.get("last_workfile_path")
    if last_workfile_path and os.path.exists(last_workfile_path):
        project_path = Path(os.path.dirname(last_workfile_path))
        unreal_project_filename = Path(os.path.basename(last_workfile_path))
    else:
        project_path = Path(os.path.join(workdir, unreal_project_name))
        project_path.mkdir(parents=True, exist_ok=True)

    self.log.info((
        f"{self.signature} requested UE version: "
        f"[ {engine_version} ]"
    ))

    # engine_path points to the specific Unreal Engine root
    # so, we are going up from the executable itself 3 levels.
    # on macOS it's 6 levels up as the executable lives under
    # ./UnrealEditor.app/Contents/MacOS/UnrealEditor
    if platform.system().lower() == "darwin":
        engine_path: Path = Path(executable).parents[6]
    else:
        engine_path: Path = Path(executable).parents[3]

    # Check if new env variable exists, and if it does, if the path
    # actually contains the plugin. If not, install it.

    built_plugin_path = self.launch_context.env.get(
        "AYON_BUILT_UNREAL_PLUGIN", None)

    if unreal_lib.check_built_plugin_existance(built_plugin_path):
        self.log.info((
            f"{self.signature} using existing built Ayon plugin from "
            f"{built_plugin_path}"
        ))
        unreal_lib.copy_built_plugin(engine_path, Path(built_plugin_path))
    else:
        # Set "AYON_UNREAL_PLUGIN" to current process environment for
        # execution of `create_unreal_project`
        env_key = "AYON_UNREAL_PLUGIN"
        if self.launch_context.env.get(env_key):
            self.log.info((
                f"{self.signature} using Ayon plugin from "
                f"{self.launch_context.env.get(env_key)}"
            ))
        if self.launch_context.env.get(env_key):
            os.environ[env_key] = self.launch_context.env[env_key]

        if not unreal_lib.check_plugin_existence(engine_path):
            self.exec_plugin_install(engine_path)

    project_file = project_path / unreal_project_filename

    self.launch_context.env["AYON_UNREAL_VERSION"] = engine_version

    if not project_file.is_file():

        # Get project settings -> allow project creation
        current_project = get_current_project_name()
        unreal_settings = get_project_settings(current_project).get("unreal")
        allow_project_creation = unreal_settings["project_setup"].get(
        "allow_project_creation")
        # add the project template options
        # add the custom path for the existing project
        if allow_project_creation:
            existing_uproject_directory = Path(
                unreal_settings["project_setup"].get(
                    "existing_uproject_directory")
            )
            uproject_files = list(existing_uproject_directory.glob("*.uproject"))
            if (
                existing_uproject_directory.exists() and
                uproject_files
            ):
                self.copy_project(existing_uproject_directory, project_path)
                # rename the project folder copied from existing_uproject directory
                new_project_path = project_path.parent / unreal_project_name
                project_path.rename(new_project_path)

                # find the copied uproject file in the new project directory
                copied_uproject_files = list(new_project_path.glob("*.uproject"))
                if len(copied_uproject_files) != 1:
                    raise ApplicationLaunchFailed(
                        f"{self.signature} Expected exactly one .uproject file in "
                        f"{new_project_path}, but found {len(copied_uproject_files)}. "
                        "Please check the project directory."
                    )
                copied_uproject_file = copied_uproject_files[0]
                # set the correct engine version on the copied file
                self.set_engine_version(copied_uproject_file, engine_version)

                # rename the copied uproject file to match the expected filename
                copied_uproject_file.rename(new_project_path / unreal_project_filename)
                self.log.info((
                    f"{self.signature} Renamed {copied_uproject_file.name} to "
                    f"{unreal_project_filename}"
                ))
            else:
                with tempfile.TemporaryDirectory() as temp_dir:
                    self.exec_ue_project_gen(engine_version,
                                             unreal_project_name,
                                             engine_path,
                                             Path(temp_dir))
                    self.copy_project(Path(temp_dir), project_path)

        # if the template path has been found with unreal project
        # copy that existing project to ayon work directory
        elif unreal_settings["project_setup"].get(
                "force_existing_project"):
            msg = (
                "Could not open project; Project file not found.\n\n"
                f"'{project_path.as_posix()}' \n\n"
                "Please contact administrator.\n"
                "Make sure the project is in the correct folder. "
                "Or enable 'allow project creation' in studio "
                "settings."
            )
            raise ApplicationLaunchFailed(msg)
        else:
            return

    # Append the project file to launch arguments
    self.launch_context.launch_args.append(
        f"\"{project_file.as_posix()}\"")

set_engine_version(uproject_path, new_version)

Set the engine version in a Unreal project file.

Parameters:

Name Type Description Default
uproject_path Path

The path to the .uproject file.

required
new_version str

The new engine version to set.

required

Raises:

Type Description
FileNotFoundError

If the .uproject file does not exist.

Source code in client/ayon_unreal/hooks/pre_workfile_preparation.py
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
def set_engine_version(self, uproject_path: Path, new_version: str):
    """Set the engine version in a Unreal project file.

    Args:
        uproject_path (Path): The path to the .uproject file.
        new_version (str): The new engine version to set.

    Raises:
        FileNotFoundError: If the .uproject file does not exist.
    """
    if not uproject_path.is_file():
        raise FileNotFoundError(f"File not found: {uproject_path}")

    try:
        data = json.loads(uproject_path.read_text(encoding="utf-8"))

    except json.JSONDecodeError as e:
        raise ApplicationLaunchFailed(
            f"{self.signature} Malformed .uproject file at {uproject_path}: {e}"
        ) from e

    # Set the new engine version
    data["EngineAssociation"] = new_version

    uproject_path.write_text(json.dumps(data, indent=4), encoding="utf-8")

    self.log.info(
        f"Engine version set to '{new_version}' for {uproject_path}"
    )