Skip to content
20 changes: 20 additions & 0 deletions src/deadline/unreal_submitter/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ class DeadlineCloudSubmitterException(Exception):
pass


class NoUINotification:
"""Marker mixin: Exceptions that shouldn't trigger UI notifications"""

pass


class ParametersAreNotConsistentError(DeadlineCloudSubmitterException):
"""Raised when OpenJD parameters/variables are not consistent"""

Expand All @@ -31,6 +37,20 @@ class OpenJobIsMissingError(DeadlineCloudSubmitterException):
pass


class InvalidUEVersionInCondaPackageParameter(DeadlineCloudSubmitterException):
"""Raised when Conda Package parameter contains an invalid UE version"""

pass


class InvalidUEVersionInCondaPackageParameterNoUI(
InvalidUEVersionInCondaPackageParameter, NoUINotification
):
"""Raised when Conda Package parameter contains an invalid UE version"""

pass


class RenderArgumentsTypeNotSetError(DeadlineCloudSubmitterException):
"""Raised when the render arguments type is not set"""

Expand Down
4 changes: 4 additions & 0 deletions src/deadline/unreal_submitter/submitter.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
UnrealOpenJob,
RenderUnrealOpenJob,
)
from deadline.unreal_submitter.exceptions import NoUINotification

from ._version import version

Expand Down Expand Up @@ -54,6 +55,9 @@ def wrapper(self, *args, **kwargs):
from_gui=not self._silent_mode,
)

if isinstance(e, NoUINotification):
return

message = notify_prefix + str(e)
if with_traceback:
message += "\n" + traceback.format_exc()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,11 @@ def _build_parameter_values(self) -> list:
if self._job_shared_settings:
parameter_values += self._job_shared_settings.serialize()

if not UnrealOpenJob.check_conda_package_version(parameter_values):
raise exceptions.InvalidUEVersionInCondaPackageParameterNoUI(
"CondaPackages Unreal Engine version mismatch"
)

return parameter_values

def _check_parameters_consistency(self):
Expand Down Expand Up @@ -500,6 +505,76 @@ def create_job_bundle(self):

return job_bundle_path

@staticmethod
def get_current_ue_version():
"""
Get current Unreal Engine version in x.y format

:return: Current Unreal Engine version
:rtype: str
"""

current_ue_version = unreal.SystemLibrary.get_engine_version()
logger.info(f"Current Unreal Engine version: {current_ue_version}")
current_version_match = re.search(r"\b\d+\.\d+\b", current_ue_version)
if not current_version_match:
logger.warning(f"Could not parse current UE version: {current_ue_version}")
raise exceptions.InvalidUEVersionInCondaPackageParameter(
f"Could not parse current UE version: {current_ue_version}"
)

return current_version_match.group(0)

@staticmethod
def check_conda_package_version(parameter_values: list[dict[str, Any]]) -> bool:
"""
Check if the CondaPackages parameter contains Unreal Engine version and compare with current UE version.

:return: True if version check passes or user confirms, False if user cancels
:rtype: bool
"""

conda_packages_param = next(
(p for p in parameter_values if p["name"] == OpenJobParameterNames.CONDA_PACKAGES), None
)

if not conda_packages_param:
return True

current_version = UnrealOpenJob.get_current_ue_version()

conda_packages_value = conda_packages_param.get("value", "")
if not conda_packages_value:
conda_packages_param["value"] = f"unrealengine={current_version}"
return True

# Check for unrealengine=x.x pattern
ue_version_match = re.search(r"unrealengine=(\d+\.\d+)", conda_packages_value)
if not ue_version_match:
conda_packages_param["value"] = (
f"unrealengine={current_version} " + conda_packages_value
)
return True

template_ue_version = ue_version_match.group(1)
logger.info(f"Template specifies Unreal Engine version: {template_ue_version}")

# Compare versions
if not template_ue_version == current_version:
# Versions don't match
result = unreal.EditorDialog.show_message(
"Version Mismatch Warning",
f"You are attempting to render a UE {current_version} project using UE {template_ue_version}. Do you wish to continue?",
unreal.AppMsgType.YES_NO,
unreal.AppReturnType.YES,
)

if result != unreal.AppReturnType.YES:
Copy link
Contributor

@Cherie-Chen Cherie-Chen Sep 19, 2025

Choose a reason for hiding this comment

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

Same feed back as the video recording above. No need to raise error here.

Copy link
Contributor

Choose a reason for hiding this comment

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

After following the code, I can see why you wanted to throw an exception here now. The alternative is handling this in multiple places along the code path. I agree throwing an error here is the easier solution, please put that code back, I apologize for the bad suggestion. However, could you please rename the exception from InvalidUEVersionInCondaPackageParameterNoUI to UserCancelledSubmission_MissmatchedUEVersion to better reflect that this is a user-initiated cancellation due to UE version mismatch? This name better communicates the actual scenario being handled.

return False

logger.info("Unreal Engine versions match, continuing with submission")
return True


# Render Open Job
class RenderUnrealOpenJob(UnrealOpenJob):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
from dataclasses import dataclass
from abc import ABC, abstractmethod
from typing import Type, Union, Literal, Optional

from openjd.model import parse_model

from openjd.model.v2023_09 import (
Expand Down Expand Up @@ -279,6 +278,7 @@ class OpenJobParameterNames:
UNREAL_EXTRA_CMD_ARGS_FILE = "ExtraCmdArgsFile"
UNREAL_EXECUTABLE_RELATIVE_PATH = "ExecutableRelativePath"
UNREAL_MRQ_JOB_DEPENDENCIES_DESCRIPTOR = "MrqJobDependenciesDescriptor"
CONDA_PACKAGES = "CondaPackages"

PERFORCE_STREAM_PATH = "PerforceStreamPath"
PERFORCE_CHANGELIST_NUMBER = "PerforceChangelistNumber"
Expand Down
51 changes: 51 additions & 0 deletions src/unreal_plugin/Content/Python/job_library.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.

import os
import re

import unreal

from deadline.unreal_submitter import common
Expand All @@ -11,6 +13,7 @@
)

from deadline.unreal_submitter.unreal_open_job.unreal_open_job import UnrealOpenJob
from deadline.unreal_submitter.unreal_open_job.unreal_open_job_entity import OpenJobParameterNames

logger = get_logger()

Expand Down Expand Up @@ -49,6 +52,54 @@ def get_job_dependencies(self, mrq_job):

return [common.os_path_from_unreal_path(d, with_ext=True) for d in unreal_dependencies]

@unreal.ufunction(override=True)
def validate_mrq_job_parameters(
self, parameters: list[unreal.ParameterDefinition]
) -> list[unreal.ParameterDefinition]:
conda_packages_param = None
conda_packages_param_index = 0
for i, p in enumerate(parameters):
if p.get_editor_property("Name") == OpenJobParameterNames.CONDA_PACKAGES:
conda_packages_param = p
conda_packages_param_index = i
break

if not conda_packages_param:
return parameters

current_version = UnrealOpenJob.get_current_ue_version()

conda_packages_value = conda_packages_param.value
if not conda_packages_value:
conda_packages_param.value = f"unrealengine={current_version}"
parameters[conda_packages_param_index] = conda_packages_param
return parameters

# Check for unrealengine=x.x pattern
ue_version_match = re.search(r"unrealengine=(\d+\.\d+)", conda_packages_value)
if not ue_version_match:
conda_packages_param.get_editor_property(
"Value", f"unrealengine={current_version} " + conda_packages_value
)
parameters[conda_packages_param_index] = conda_packages_param
return parameters

template_ue_version = ue_version_match.group(1)
logger.info(f"Template specifies Unreal Engine version: {template_ue_version}")

# Compare versions
if not template_ue_version == current_version:
# replace with current version
conda_packages_param.value = re.sub(
r"unrealengine=\d+\.\d+",
f"unrealengine={current_version}",
conda_packages_value,
)
logger.info(f"Updated Unreal Engine version in conda packages to: {current_version}")
parameters[conda_packages_param_index] = conda_packages_param

return parameters

@unreal.ufunction(override=True)
def get_plugins_dependencies(self):
return [d for d in UnrealOpenJob.get_plugins_references().input_directories]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,16 @@ void UMoviePipelineDeadlineCloudExecutorJob::JobPresetChanged()

this->JobTemplateOverrides.StepsOverrides = GetStepsToOverride(SelectedJobPreset);
this->JobTemplateOverrides.EnvironmentsOverrides = GetEnvironmentsToOverride(SelectedJobPreset);

UDeadlineCloudJobBundleLibrary* Library = UDeadlineCloudJobBundleLibrary::Get();
if (Library)
{
JobTemplateOverrides.Parameters = Library->ValidateMrqJobParameters(JobTemplateOverrides.Parameters);
}
else
{
UE_LOG(LogTemp, Error, TEXT("Error get DeadlineCloudJobBundleLibrary"));
}
}

void UMoviePipelineDeadlineCloudExecutorJob::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ class UNREALDEADLINECLOUDSERVICE_API UDeadlineCloudJobBundleLibrary : public UOb
UFUNCTION(BlueprintImplementableEvent)
TArray<FString> GetJobDependencies(const UMoviePipelineDeadlineCloudExecutorJob *MrqJob);

/**
* Validate MRQ job parameters
* @param MrqJob Unreal MRQ job
*/
UFUNCTION(BlueprintImplementableEvent)
TArray<FParameterDefinition> ValidateMrqJobParameters(const TArray<FParameterDefinition>& Parameters);

/**
* Collect list of required plugins for the job
* @return List of the plugins dependencies
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.

import sys
from typing import Any

from unittest.mock import MagicMock, patch

unreal_mock = MagicMock()
sys.modules["unreal"] = unreal_mock

from deadline.unreal_submitter.unreal_open_job.unreal_open_job import ( # noqa: E402
UnrealOpenJob,
)

from deadline.unreal_submitter.unreal_open_job.unreal_open_job_entity import ( # noqa: E402
OpenJobParameterNames,
)


class TestCheckCondaPackageVersion:

@patch(
"deadline.unreal_submitter.unreal_open_job.unreal_open_job.unreal.SystemLibrary.get_engine_version"
)
def test_check_conda_package_version_no_conda_packages_param(self, get_engine_version_mock):
# GIVEN
get_engine_version_mock.return_value = "5.4.0"

job_parameter_values: list[dict[str, Any]] = []

# WHEN
assert UnrealOpenJob.check_conda_package_version(job_parameter_values)

# THEN
assert job_parameter_values == []

@patch(
"deadline.unreal_submitter.unreal_open_job.unreal_open_job.unreal.SystemLibrary.get_engine_version"
)
def test_check_conda_package_version_conda_packages_no_value(self, get_engine_version_mock):
# GIVEN
get_engine_version_mock.return_value = "5.3.0"

job_parameter_values = [dict(name=OpenJobParameterNames.CONDA_PACKAGES, value="")]

# WHEN
assert UnrealOpenJob.check_conda_package_version(job_parameter_values)

# THEN
assert job_parameter_values[0].get("value") == "unrealengine=5.3"

@patch(
"deadline.unreal_submitter.unreal_open_job.unreal_open_job.unreal.SystemLibrary.get_engine_version"
)
def test_check_conda_package_version_no_unrealengine_pattern(self, get_engine_version_mock):
# GIVEN
get_engine_version_mock.return_value = "5.3.0"

job_parameter_values = [
dict(name=OpenJobParameterNames.CONDA_PACKAGES, value="somepackage=1.0")
]

# WHEN
assert UnrealOpenJob.check_conda_package_version(job_parameter_values)

# THEN
assert job_parameter_values[0].get("value") == "unrealengine=5.3 somepackage=1.0"

@patch(
"deadline.unreal_submitter.unreal_open_job.unreal_open_job.unreal.SystemLibrary.get_engine_version"
)
def test_check_conda_package_version_versions_match(self, get_engine_version_mock):
# GIVEN
get_engine_version_mock.return_value = "5.3"

job_parameter_values = [
dict(name=OpenJobParameterNames.CONDA_PACKAGES, value="unrealengine=5.3")
]

# WHEN
assert UnrealOpenJob.check_conda_package_version(job_parameter_values)

@patch(
"deadline.unreal_submitter.unreal_open_job.unreal_open_job.unreal.SystemLibrary.get_engine_version"
)
def test_check_conda_package_version_versions_mismatch(self, get_engine_version_mock):
# GIVEN
get_engine_version_mock.return_value = "5.4"

job_parameter_values = [
dict(name=OpenJobParameterNames.CONDA_PACKAGES, value="unrealengine=5.3")
]

# WHEN
assert not UnrealOpenJob.check_conda_package_version(job_parameter_values)

@patch(
"deadline.unreal_submitter.unreal_open_job.unreal_open_job.unreal.SystemLibrary.get_engine_version"
)
def test_check_conda_package_version_minor_version_difference(self, get_engine_version_mock):
# GIVEN
get_engine_version_mock.return_value = "5.3.2"

job_parameter_values = [
dict(name=OpenJobParameterNames.CONDA_PACKAGES, value="unrealengine=5.3")
]

# WHEN
assert UnrealOpenJob.check_conda_package_version(job_parameter_values)
Original file line number Diff line number Diff line change
Expand Up @@ -125,12 +125,18 @@ def test__find_extra_parameter(
# THEN
assert isinstance(param, UnrealOpenJobParameterDefinition) == found

@patch(
"deadline.unreal_submitter.unreal_open_job.unreal_open_job.unreal.SystemLibrary.get_engine_version",
return_value="5.4",
)
@patch(
"deadline.unreal_submitter.unreal_open_job.unreal_open_job_entity."
"UnrealOpenJobEntity.get_template_object",
return_value=fixtures.f_job_template_default(),
)
def test__build_parameter_values(self, get_template_object_mock: Mock):
def test__build_parameter_values(
self, get_engine_version: Mock, get_template_object_mock: Mock
):
# GIVEN
yaml_parameters = fixtures.f_job_template_default()["parameterDefinitions"]
open_job = UnrealOpenJob(
Expand Down