Skip to content

Conversation

@Kryses
Copy link

@Kryses Kryses commented Nov 24, 2025

Changelog Description

This is an update to allow the unreal add-on to run in two modes. One being the original the requires the installation of the Ayon unreal plug-in to the project. The other storing Ayon data within Unreal native classes. The reason for this use case is in a vendor scenario where the vendor does not have control over the project in its entirety.

Functionally, Native mode works the same as Ayon plug-in, with the exception that in Native mode it will not be able to create a new unreal project. Ayon Unreal use a commandlet provided by the Ayon plug-in in order to create the project.

Some additional note are that prior to Unreal Engine 5.6. You cannot create new properies on a Blueprint class using the python api. For the publish instances to function in Ayon they must have asset_data_internal, add_external_asset and asset_data_external. Because of this the blueprint assets will need to be created for your version of unreal.

Warning

This is not designed to be interchangeable with the Ayon Plugin if
a project is started with the Ayon Plugin it should remain in the plugin mode.
likewise if a project is started in Native mode it should remain in Native mode

Changes

  • When Unreal Plug-in is disabled the addon will create base blueprint classes in /Game/Ayon/AyonContainerTypes . These are used to generate Containters and Publish instance from the Ayon tools like the Loader and the Publisher.
image
  • Functionality in ayon_unreal/api/pipeline.py has been moved in to classes that live in ayon_unreal/api/backends.

Additional review information

Detailed information of the changes made to the product or service, providing an in-depth description of the updates and enhancements. This can include technical information, code examples and anything else needed for the review of the PR.

Added Settings

image
  • Use Unreal Plugin: Tells the addon to use the Ayon Unreal Plug-in or not
    ayon+settings://unreal/project_setup/use_plugin
  • Use Exact Path: If this is set the addon will use the exact that to the .uproject file using basic anatomy templates.
    ayon+settings://unreal/project_setup/use_exact_path

Testing notes:

  1. Create a project from unreal to generate a .uproject file.
  2. In the Ayon Unreal settings turn off "Use Unreal Plugin".
  3. Set "Use Exact Path" to be enable
  4. Set "Use Existing UProject for Project Creation" to the path to you're .uproject file. This can either be a full path or a templated path for example root[unreal]/unreal/testproject.uproject
  5. When the project launches use the Ayon provided tools to preform your workflow as normal.

@antirotor antirotor added type: feature Adding something new and exciting to the product community Issues and PRs coming from the community members labels Dec 11, 2025
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces dual-mode operation for the Unreal addon, allowing it to run either with the Ayon Unreal plugin (original mode) or using native Unreal classes (new mode). This enables vendor scenarios where full project control isn't available. The implementation adds a backend architecture pattern to abstract plugin vs. native functionality, includes new settings for mode selection and exact path usage, and provides blueprint asset copying for versions prior to UE 5.6 where Python API limitations exist.

Key changes:

  • Added backend abstraction layer (AyonPluginBackend and NativeUnrealBackend) to support both modes
  • Introduced new settings: use_plugin and use_exact_path for project configuration
  • Implemented blueprint copying hook for native mode support in Unreal versions below 5.6

Reviewed changes

Copilot reviewed 15 out of 17 changed files in this pull request and generated 27 comments.

Show a summary per file
File Description
server/settings.py Adds use_plugin and use_exact_path settings to ProjectSetup with default values
client/ayon_unreal/startup/init_unreal.py New startup script for Unreal that handles Ayon host installation with error handling for missing dependencies
client/ayon_unreal/hooks/pre_workfile_preparation.py Updates hook to support both plugin and native modes, adds exact path configuration, and sets environment variables accordingly
client/ayon_unreal/hooks/pre_copy_blueprints.py New hook to copy blueprint assets to project for native mode in UE versions < 5.6
client/ayon_unreal/blueprints/UE_5.3/AyonPublishInstance.uasset Binary blueprint asset for publish instances in native mode
client/ayon_unreal/blueprints/UE_5.3/AyonAssetContainer.uasset Binary blueprint asset for containers in native mode
client/ayon_unreal/api/tools_ui.py Code formatting improvements and import reorganization
client/ayon_unreal/api/rendering.py Updates class name check to support both plugin and native blueprint classes
client/ayon_unreal/api/plugin.py Changes from add() to append() for asset list handling
client/ayon_unreal/api/pipeline.py Refactors to use backend abstraction, moves constants to separate file, removes hardcoded implementation details
client/ayon_unreal/api/menu.py New menu system implementation using Unreal's ToolMenus API
client/ayon_unreal/api/constants.py Extracts constants from pipeline.py for better organization
client/ayon_unreal/api/backends/native_unreal.py Implements native Unreal backend using blueprint-based containers and publish instances
client/ayon_unreal/api/backends/base.py Defines abstract backend interface for plugin and native implementations
client/ayon_unreal/api/backends/ayon_plugin.py Implements plugin backend using Ayon plugin factories
client/ayon_unreal/api/backends/init.py Provides backend selection logic based on AYON_PLUGIN_ENABLED environment variable
client/ayon_unreal/addon.py Adds startup path to Python path and menu initialization on host install

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

f"{self.signature} using Ayon plugin from "
f"{self.launch_context.env.get(env_key)}"
))
if self.launch_context.env.get(env_key):
Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This condition is checked twice in a row on lines 256 and 261. The duplicate check on line 261 appears to be unnecessary since the same condition was just checked on line 256. This could indicate a logic error where a different condition was intended for the second check.

Suggested change
if self.launch_context.env.get(env_key):

Copilot uses AI. Check for mistakes.

project_path = self.launch_context.env.get("AYON_UNREAL_PROJECT_PATH")
if not project_path:
raise RuntimeError("AYON_UNREAL_PROJECT_PATH not set.")
Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message "AYON_UNREAL_PROJECT_PATH not set." should be more descriptive and provide context about when this error occurs and potential solutions. Consider adding information about which hook is raising this error and suggesting that the project path should have been set by the pre_workfile_preparation hook.

Suggested change
raise RuntimeError("AYON_UNREAL_PROJECT_PATH not set.")
raise RuntimeError(
"AYON_UNREAL_PROJECT_PATH not set in environment during CopyBlueprints pre-launch hook. "
"This usually means the Unreal project path was not set by the pre_workfile_preparation hook. "
"Please ensure that the pre_workfile_preparation hook runs and sets AYON_UNREAL_PROJECT_PATH before launching Unreal."
)

Copilot uses AI. Check for mistakes.
Comment on lines 36 to 47
blueprint_files = unreal_blueprint_path.glob('*.uasset')

container_path.mkdir(exist_ok=True, parents=True)
for blueprint_file in blueprint_files:
dest = container_path.joinpath(blueprint_file.name)
if dest.exists():
if not filecmp.cmp(blueprint_file, dest):
shutil.copy(blueprint_file, dest)
else:
continue
else:
shutil.copy(blueprint_file, dest)
Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The blueprint_files generator from glob is not checked for existence. If the blueprint directory doesn't exist or contains no files, the code will silently succeed without copying any blueprints. Consider adding a check to warn or error if no blueprint files are found, especially since this is a critical step for Native mode to function properly.

Copilot uses AI. Check for mistakes.
project_path = project_file.parent
self.log.info(f"New Project File {project_file}")
if not project_file.is_file():
raise RuntimeError("Invalid Project Path")
Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message "Invalid Project Path" is too generic. It should include the actual path that was attempted and provide more context about what conditions need to be met. Consider including the resolved project_file path in the error message.

Suggested change
raise RuntimeError("Invalid Project Path")
raise RuntimeError(
f"Invalid Project Path: '{project_file}'. The file does not exist or is not a valid Unreal project file."
)

Copilot uses AI. Check for mistakes.
data_asset_class = unreal.load_class(
None, "/Game/Ayon/AyonContainerTypes/AyonAssetContainer.AyonAssetContainer_C"
)
print(f"Creating Ayon Container {container}")
Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debug print statement should be replaced with proper logging. Consider using a logger instance instead of print for better control and consistency with the rest of the codebase.

Copilot uses AI. Check for mistakes.
Comment on lines +287 to +293
project_file = pathlib.Path(project_template.format_strict(template_data))
project_path = project_file.parent
self.log.info(f"New Project File {project_file}")
if not project_file.is_file():
raise RuntimeError("Invalid Project Path")
else:
project_file = project_path / unreal_project_filename
Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When use_exact_path is True, the code sets project_file and project_path but then continues to check if the project file exists (line 298). If the file doesn't exist, it may enter the project creation logic (lines 298-363), but this logic expects project_path to be set based on workdir/unreal_project_name (line 212), not from the exact path. This could cause the wrong project_path to be set in AYON_UNREAL_PROJECT_PATH at line 365, leading to blueprint copying failures in the CopyBlueprints hook.

Copilot uses AI. Check for mistakes.
for member in pre_create_data.get("members", []):
obj = ar.get_asset_by_object_path(member).get_asset()
assets.add(obj)
assets.append(obj)
Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The change from assets.add(obj) to assets.append(obj) suggests that asset_data_external is now a list instead of a set. However, this change could introduce duplicate entries if the same asset is added multiple times, whereas a set would naturally prevent duplicates. Consider whether duplicate prevention logic is needed, or if this behavioral change is intentional.

Suggested change
assets.append(obj)
if obj not in assets:
assets.append(obj)

Copilot uses AI. Check for mistakes.
return instances

@staticmethod
def containerise():
Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method requires 0 positional arguments, whereas overridden UnrealBackend.containerise requires at least 4.

Suggested change
def containerise():
def containerise(arg1, arg2, arg3, arg4):

Copilot uses AI. Check for mistakes.
# Copyright (c) 2024 Ynput s.r.o.
import unreal
try:
import qtpy #noqa F401
Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Import of 'qtpy' is not used.

Copilot uses AI. Check for mistakes.
import unreal
try:
import qtpy #noqa F401
from qtpy import QtWidgets #noqa F401
Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Import of 'QtWidgets' is not used.

Suggested change
from qtpy import QtWidgets #noqa F401

Copilot uses AI. Check for mistakes.
@moonyuet
Copy link
Member

moonyuet commented Dec 12, 2025

@Kryses Thank you for this contribution. Can you tell specifically why it does not work in 5.6 (as it is working in my side for loading all the ayon-related data correctly)? I mean I do understand user can't create new properties on a blueprint class by using python api, but in terms of what particular aspect, it would affect how the ayon data works?
I tested in 5.5 and 5.6, they are all working, and I already turned off the use_plugins.


def on_host_install(self, host, host_name, project_name):
self.log.info("Starting Host")
if host_name == 'unreal':
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NOTE: Unreal host integration takes care about calling install, so it can be handled there, instead of handling it on addon level. (UnrealHost.install in client/ayon_unreal/api/pipeline.py)

Comment on lines +10 to +12
UNREAL_VERSION = semver.VersionInfo(
*os.getenv("AYON_UNREAL_VERSION").split(".")
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
UNREAL_VERSION = semver.VersionInfo(
*os.getenv("AYON_UNREAL_VERSION").split(".")
)
UNREAL_VERSION = semver.VersionInfo.parse(
os.getenv("AYON_UNREAL_VERSION")
)

This file is used only inside unreal, doesn't have unreal dedicated function to get unreal version instead of using AYON_UNREAL_VERSION?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does, but this is primarily used in the pre-launch hooks before we run unreal. The copy might be able to run in the host install but, I'm not entirely sure if unreal will react to adding the uassets after it has been initialized.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So after doing some testing on this. The AYON_UNREAL_VERSION is set by the application variant in ayon-applications I believe. The defaults are <major>.<minor> in the current version of semver we're using VersionInfo.parse is expecting <major>.<minor>.<patch>. Newer versions of semver have a new argment for optional_minor_and_patch

I'm trying to find away to be less disruptive. If we continue with this version of semver. People might need to update the names of their variants. If we upgrade the version of semver they will need to run the dependencies tool.

What are your thoughts on this?

image



@unreal.uclass()
class AyonPythonMenuTool(unreal.ToolMenuEntryScript):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be AYON instead of Ayon (if it's new).

yield cast_map_to_str_dict(data)



Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change

Comment on lines +252 to +262
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]
Copy link
Member

@iLLiCiTiT iLLiCiTiT Dec 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really don't think we should set this to current process env, it will affect all future launches.

Suggested change
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]
elif self.launch_context.env.get("AYON_UNREAL_PLUGIN"):
# Set "AYON_UNREAL_PLUGIN" to current process environment for
# execution of `create_unreal_project`
ay_unreal_plugin = self.launch_context.env.get("AYON_UNREAL_PLUGIN")
self.log.info(
f"{self.signature} using AYON plugin from {ay_unreal_plugin}"
)
os.environ["AYON_UNREAL_PLUGIN"] = ay_unreal_plugin

@Kryses
Copy link
Author

Kryses commented Dec 15, 2025

@Kryses Thank you for this contribution. Can you tell specifically why it does not work in 5.6 (as it is working in my side for loading all the ayon-related data correctly)? I mean I do understand user can't create new properties on a blueprint class by using python api, but in terms of what particular aspect, it would affect how the ayon data works? I tested in 5.5 and 5.6, they are all working, and I already turned off the use_plugins.

It's not necessarily that it won't work in 5.5 or 5.6. I hadn't had a chance to test it in those versions. Because I'm using uassets made from 5.3 I wasn't sure if it would be as simple as copying the uassets into the AyonContainers location. I figured there might be some problems using those assets when going into a newer version.

I'd say if it works than it works in this case.

Kryses and others added 14 commits December 15, 2025 09:31
Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com>
Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com>
Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com>
Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com>
Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com>
Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com>
Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com>
Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com>
Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com>
Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com>
…Entertainment/ayon-unreal into feature/remove-plugin-requirement
if not env.get("AYON_UNREAL_PLUGIN"):
env["AYON_UNREAL_PLUGIN"] = unreal_plugin_path


Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change

if not env.get(key):
env[key] = value


Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

community Issues and PRs coming from the community members type: feature Adding something new and exciting to the product

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants