Merge branch 'main' into Atom/dmcdiar/ATOM-15517

main
Doug McDiarmid 5 years ago
commit 35d7ee5e53

@ -0,0 +1,10 @@
"""
All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
its licensors.
For complete copyright and license terms please see the LICENSE at the root of this
distribution (the "License"). All use of this software is governed by the License,
or, if provided, by the license below or the license accompanying this file. Do not
remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
"""

@ -0,0 +1,237 @@
"""
All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
its licensors.
For complete copyright and license terms please see the LICENSE at the root of this
distribution (the "License"). All use of this software is governed by the License,
or, if provided, by the license below or the license accompanying this file. Do not
remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
"""
import logging
import os
import pytest
import time
import typing
from datetime import datetime
import ly_test_tools.log.log_monitor
from assetpipeline.ap_fixtures.asset_processor_fixture import asset_processor as asset_processor
from AWS.common.aws_utils import aws_utils
from AWS.common.aws_credentials import aws_credentials
from AWS.Windows.resource_mappings.resource_mappings import resource_mappings
from AWS.Windows.cdk.cdk import cdk
from .aws_metrics_utils import aws_metrics_utils
AWS_METRICS_FEATURE_NAME = 'AWSMetrics'
GAME_LOG_NAME = 'Game.log'
logger = logging.getLogger(__name__)
def setup(launcher: ly_test_tools.launchers.Launcher,
cdk: cdk,
asset_processor: asset_processor,
resource_mappings: resource_mappings,
context_variable: str = '') -> typing.Tuple[ly_test_tools.log.log_monitor.LogMonitor, str, str]:
"""
Set up the CDK application and start the log monitor.
:param launcher: Client launcher for running the test level.
:param cdk: CDK application for deploying the AWS resources.
:param asset_processor: asset_processor fixture.
:param resource_mappings: resource_mappings fixture.
:param context_variable: context_variable for enable optional CDK feature.
:return log monitor object, metrics file path and the metrics stack name.
"""
logger.info(f'Cdk stack names:\n{cdk.list()}')
stacks = cdk.deploy(context_variable=context_variable)
resource_mappings.populate_output_keys(stacks)
asset_processor.start()
asset_processor.wait_for_idle()
metrics_file_path = os.path.join(launcher.workspace.paths.project(), 'user',
AWS_METRICS_FEATURE_NAME, 'metrics.json')
remove_file(metrics_file_path)
file_to_monitor = os.path.join(launcher.workspace.paths.project_log(), GAME_LOG_NAME)
remove_file(file_to_monitor)
# Initialize the log monitor.
log_monitor = ly_test_tools.log.log_monitor.LogMonitor(launcher=launcher, log_file_path=file_to_monitor)
return log_monitor, metrics_file_path, stacks[0]
def monitor_metrics_submission(log_monitor: ly_test_tools.log.log_monitor.LogMonitor) -> None:
"""
Monitor the messages and notifications for submitting metrics.
:param log_monitor: Log monitor to check the log messages.
"""
expected_lines = [
'(Script) - Submitted metrics without buffer.',
'(Script) - Submitted metrics with buffer.',
'(Script) - Metrics is sent successfully.'
]
unexpected_lines = [
'(Script) - Failed to submit metrics without buffer.',
'(Script) - Failed to submit metrics with buffer.',
'(Script) - Failed to send metrics.'
]
result = log_monitor.monitor_log_for_lines(
expected_lines=expected_lines,
unexpected_lines=unexpected_lines,
halt_on_unexpected=True)
# Assert the log monitor detected expected lines and did not detect any unexpected lines.
assert result, (
f'Log monitoring failed. Used expected_lines values: {expected_lines} & '
f'unexpected_lines values: {unexpected_lines}')
def remove_file(file_path: str) -> None:
"""
Remove a local file and its directory.
:param file_path: Path to the local file.
"""
if os.path.exists(file_path):
os.remove(file_path)
file_dir = os.path.dirname(file_path)
if os.path.exists(file_dir) and len(os.listdir(file_dir)) == 0:
os.rmdir(file_dir)
@pytest.mark.SUITE_periodic
@pytest.mark.usefixtures('automatic_process_killer')
@pytest.mark.parametrize('project', ['AutomatedTesting'])
@pytest.mark.parametrize('level', ['AWS/Metrics'])
@pytest.mark.parametrize('feature_name', [AWS_METRICS_FEATURE_NAME])
@pytest.mark.parametrize('resource_mappings_filename', ['aws_resource_mappings.json'])
@pytest.mark.parametrize('profile_name', ['AWSAutomationTest'])
@pytest.mark.parametrize('region_name', ['us-west-2'])
@pytest.mark.parametrize('assume_role_arn', ['arn:aws:iam::645075835648:role/o3de-automation-tests'])
@pytest.mark.parametrize('session_name', ['o3de-Automation-session'])
class TestAWSMetrics_Windows(object):
def test_AWSMetrics_RealTimeAnalytics_MetricsSentToCloudWatch(self,
level: str,
launcher: ly_test_tools.launchers.Launcher,
asset_processor: pytest.fixture,
workspace: pytest.fixture,
aws_utils: aws_utils,
aws_credentials: aws_credentials,
resource_mappings: resource_mappings,
cdk: cdk,
aws_metrics_utils: aws_metrics_utils,
):
"""
Tests that the submitted metrics are sent to CloudWatch for real-time analytics.
"""
log_monitor, metrics_file_path, stack_name = setup(launcher, cdk, asset_processor, resource_mappings)
# Start the Kinesis Data Analytics application for real-time analytics.
analytics_application_name = f'{stack_name}-AnalyticsApplication'
aws_metrics_utils.start_kinesis_data_analytics_application(analytics_application_name)
launcher.args = ['+LoadLevel', level]
launcher.args.extend(['-rhi=null'])
with launcher.start(launch_ap=False):
start_time = datetime.utcnow()
monitor_metrics_submission(log_monitor)
# Verify that operational health metrics are delivered to CloudWatch.
aws_metrics_utils.verify_cloud_watch_delivery(
'AWS/Lambda',
'Invocations',
[{'Name': 'FunctionName',
'Value': f'{stack_name}-AnalyticsProcessingLambda'}],
start_time)
logger.info('Operational health metrics sent to CloudWatch.')
aws_metrics_utils.verify_cloud_watch_delivery(
AWS_METRICS_FEATURE_NAME,
'TotalLogins',
[],
start_time)
logger.info('Real-time metrics sent to CloudWatch.')
# Stop the Kinesis Data Analytics application.
aws_metrics_utils.stop_kinesis_data_analytics_application(analytics_application_name)
def test_AWSMetrics_UnauthorizedUser_RequestRejected(self,
level: str,
launcher: ly_test_tools.launchers.Launcher,
cdk: cdk,
aws_credentials: aws_credentials,
asset_processor: pytest.fixture,
resource_mappings: resource_mappings,
workspace: pytest.fixture):
"""
Tests that unauthorized users cannot send metrics events to the AWS backed backend.
"""
log_monitor, metrics_file_path, stack_name = setup(launcher, cdk, asset_processor, resource_mappings)
# Set invalid AWS credentials.
launcher.args = ['+LoadLevel', level, '+cl_awsAccessKey', 'AKIAIOSFODNN7EXAMPLE',
'+cl_awsSecretKey', 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY']
launcher.args.extend(['-rhi=null'])
with launcher.start(launch_ap=False):
result = log_monitor.monitor_log_for_lines(
expected_lines=['(Script) - Failed to send metrics.'],
unexpected_lines=['(Script) - Metrics is sent successfully.'],
halt_on_unexpected=True)
assert result, 'Metrics events are sent successfully by unauthorized user'
logger.info('Unauthorized user is rejected to send metrics.')
def test_AWSMetrics_BatchAnalytics_MetricsDeliveredToS3(self,
level: str,
launcher: ly_test_tools.launchers.Launcher,
cdk: cdk,
aws_credentials: aws_credentials,
asset_processor: pytest.fixture,
resource_mappings: resource_mappings,
aws_utils: aws_utils,
aws_metrics_utils: aws_metrics_utils,
workspace: pytest.fixture):
"""
Tests that the submitted metrics are sent to the data lake for batch analytics.
"""
log_monitor, metrics_file_path, stack_name = setup(launcher, cdk, asset_processor, resource_mappings,
context_variable='batch_processing=true')
analytics_bucket_name = aws_metrics_utils.get_analytics_bucket_name(stack_name)
launcher.args = ['+LoadLevel', level]
launcher.args.extend(['-rhi=null'])
with launcher.start(launch_ap=False):
start_time = datetime.utcnow()
monitor_metrics_submission(log_monitor)
# Verify that operational health metrics are delivered to CloudWatch.
aws_metrics_utils.verify_cloud_watch_delivery(
'AWS/Lambda',
'Invocations',
[{'Name': 'FunctionName',
'Value': f'{stack_name}-EventsProcessingLambda'}],
start_time)
logger.info('Operational health metrics sent to CloudWatch.')
aws_metrics_utils.verify_s3_delivery(analytics_bucket_name)
logger.info('Metrics sent to S3.')
# Run the glue crawler to populate the AWS Glue Data Catalog with tables.
aws_metrics_utils.run_glue_crawler(f'{stack_name}-EventsCrawler')
# Run named queries on the table to verify the batch analytics.
aws_metrics_utils.run_named_queries(f'{stack_name}-AthenaWorkGroup')
logger.info('Query metrics from S3 successfully.')
# Kinesis Data Firehose buffers incoming data before it delivers it to Amazon S3. Sleep for the
# default interval (60s) to make sure that all the metrics are sent to the bucket before cleanup.
time.sleep(60)
# Empty the S3 bucket. S3 buckets can only be deleted successfully when it doesn't contain any object.
aws_metrics_utils.empty_s3_bucket(analytics_bucket_name)

@ -0,0 +1,252 @@
"""
All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
its licensors.
For complete copyright and license terms please see the LICENSE at the root of this
distribution (the "License"). All use of this software is governed by the License,
or, if provided, by the license below or the license accompanying this file. Do not
remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
"""
import logging
import pathlib
import pytest
import typing
from datetime import datetime
from botocore.exceptions import WaiterError
from AWS.common.aws_utils import AwsUtils
from .aws_metrics_waiters import KinesisAnalyticsApplicationUpdatedWaiter, \
CloudWatchMetricsDeliveredWaiter, DataLakeMetricsDeliveredWaiter, GlueCrawlerReadyWaiter
logging.getLogger('boto').setLevel(logging.CRITICAL)
# Expected directory and file extension for the S3 objects.
EXPECTED_S3_DIRECTORY = 'firehose_events/'
EXPECTED_S3_OBJECT_EXTENSION = '.parquet'
class AWSMetricsUtils:
"""
Provide utils functions for the AWSMetrics gem to interact with the deployed resources.
"""
def __init__(self, aws_utils: AwsUtils):
self._aws_util = aws_utils
def start_kinesis_data_analytics_application(self, application_name: str) -> None:
"""
Start the Kenisis Data Analytics application for real-time analytics.
:param application_name: Name of the Kenisis Data Analytics application.
"""
input_id = self.get_kinesis_analytics_application_input_id(application_name)
assert input_id, 'invalid Kinesis Data Analytics application input.'
client = self._aws_util.client('kinesisanalytics')
try:
client.start_application(
ApplicationName=application_name,
InputConfigurations=[
{
'Id': input_id,
'InputStartingPositionConfiguration': {
'InputStartingPosition': 'NOW'
}
},
]
)
except client.exceptions.ResourceInUseException:
# The application has been started.
return
try:
KinesisAnalyticsApplicationUpdatedWaiter(client, 'RUNNING').wait(application_name=application_name)
except WaiterError as e:
assert False, f'Failed to start the Kinesis Data Analytics application: {str(e)}.'
def get_kinesis_analytics_application_input_id(self, application_name: str) -> str:
"""
Get the input ID for the Kenisis Data Analytics application.
:param application_name: Name of the Kenisis Data Analytics application.
:return: Input ID for the Kenisis Data Analytics application.
"""
client = self._aws_util.client('kinesisanalytics')
response = client.describe_application(
ApplicationName=application_name
)
if not response:
return ''
input_descriptions = response.get('ApplicationDetail', {}).get('InputDescriptions', [])
if len(input_descriptions) != 1:
return ''
return input_descriptions[0].get('InputId', '')
def stop_kinesis_data_analytics_application(self, application_name: str) -> None:
"""
Stop the Kenisis Data Analytics application.
:param application_name: Name of the Kenisis Data Analytics application.
"""
client = self._aws_util.client('kinesisanalytics')
client.stop_application(
ApplicationName=application_name
)
try:
KinesisAnalyticsApplicationUpdatedWaiter(client, 'READY').wait(application_name=application_name)
except WaiterError as e:
assert False, f'Failed to stop the Kinesis Data Analytics application: {str(e)}.'
def verify_cloud_watch_delivery(self, namespace: str, metrics_name: str,
dimensions: typing.List[dict], start_time: datetime) -> None:
"""
Verify that the expected metrics is delivered to CloudWatch.
:param namespace: Namespace of the metrics.
:param metrics_name: Name of the metrics.
:param dimensions: Dimensions of the metrics.
:param start_time: Start time for generating the metrics.
"""
client = self._aws_util.client('cloudwatch')
try:
CloudWatchMetricsDeliveredWaiter(client).wait(
namespace=namespace,
metrics_name=metrics_name,
dimensions=dimensions,
start_time=start_time
)
except WaiterError as e:
assert False, f'Failed to deliver metrics to CloudWatch: {str(e)}.'
def verify_s3_delivery(self, analytics_bucket_name: str) -> None:
"""
Verify that metrics are delivered to S3 for batch analytics successfully.
:param analytics_bucket_name: Name of the deployed S3 bucket.
"""
client = self._aws_util.client('s3')
bucket_name = analytics_bucket_name
try:
DataLakeMetricsDeliveredWaiter(client).wait(bucket_name=bucket_name, prefix=EXPECTED_S3_DIRECTORY)
except WaiterError as e:
assert False, f'Failed to find the S3 directory for storing metrics data: {str(e)}.'
# Check whether the data is converted to the expected data format.
response = client.list_objects_v2(
Bucket=bucket_name,
Prefix=EXPECTED_S3_DIRECTORY
)
assert response.get('KeyCount', 0) != 0, f'Failed to deliver metrics to the S3 bucket {bucket_name}.'
s3_objects = response.get('Contents', [])
for s3_object in s3_objects:
key = s3_object.get('Key', '')
assert pathlib.Path(key).suffix == EXPECTED_S3_OBJECT_EXTENSION, \
f'Invalid data format is found in the S3 bucket {bucket_name}'
def run_glue_crawler(self, crawler_name: str) -> None:
"""
Run the Glue crawler and wait for it to finish.
:param crawler_name: Name of the Glue crawler
"""
client = self._aws_util.client('glue')
try:
client.start_crawler(
Name=crawler_name
)
except client.exceptions.CrawlerRunningException:
# The crawler has already been started.
return
try:
GlueCrawlerReadyWaiter(client).wait(crawler_name=crawler_name)
except WaiterError as e:
assert False, f'Failed to run the Glue crawler: {str(e)}.'
def run_named_queries(self, work_group: str) -> None:
"""
Run the named queries under the specific Athena work group.
:param work_group: Name of the Athena work group.
"""
client = self._aws_util.client('athena')
# List all the named queries.
response = client.list_named_queries(
WorkGroup=work_group
)
named_query_ids = response.get('NamedQueryIds', [])
# Run each of the queries.
for named_query_id in named_query_ids:
get_named_query_response = client.get_named_query(
NamedQueryId=named_query_id
)
named_query = get_named_query_response.get('NamedQuery', {})
start_query_execution_response = client.start_query_execution(
QueryString=named_query.get('QueryString', ''),
QueryExecutionContext={
'Database': named_query.get('Database', '')
},
WorkGroup=work_group
)
# Wait for the query to finish.
state = 'RUNNING'
while state == 'QUEUED' or state == 'RUNNING':
get_query_execution_response = client.get_query_execution(
QueryExecutionId=start_query_execution_response.get('QueryExecutionId', '')
)
state = get_query_execution_response.get('QueryExecution', {}).get('Status', {}).get('State', '')
assert state == 'SUCCEEDED', f'Failed to run the named query {named_query.get("Name", {})}'
def empty_s3_bucket(self, bucket_name: str) -> None:
"""
Empty the S3 bucket following:
https://boto3.amazonaws.com/v1/documentation/api/latest/guide/migrations3.html
:param bucket_name: Name of the S3 bucket.
"""
s3 = self._aws_util.resource('s3')
bucket = s3.Bucket(bucket_name)
for key in bucket.objects.all():
key.delete()
def get_analytics_bucket_name(self, stack_name: str) -> str:
"""
Get the name of the deployed S3 bucket.
:param stack_name: Name of the CloudFormation stack.
:return: Name of the deployed S3 bucket.
"""
client = self._aws_util.client('cloudformation')
response = client.describe_stack_resources(
StackName=stack_name
)
resources = response.get('StackResources', [])
for resource in resources:
if resource.get('ResourceType') == 'AWS::S3::Bucket':
return resource.get('PhysicalResourceId', '')
return ''
@pytest.fixture(scope='function')
def aws_metrics_utils(
request: pytest.fixture,
aws_utils: pytest.fixture):
"""
Fixture for the AWS metrics util functions.
:param request: _pytest.fixtures.SubRequest class that handles getting
a pytest fixture from a pytest function/fixture.
:param aws_utils: aws_utils fixture.
"""
aws_utils_obj = AWSMetricsUtils(aws_utils)
return aws_utils_obj

@ -0,0 +1,142 @@
"""
All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
its licensors.
For complete copyright and license terms please see the LICENSE at the root of this
distribution (the "License"). All use of this software is governed by the License,
or, if provided, by the license below or the license accompanying this file. Do not
remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
"""
import botocore.client
import logging
from datetime import timedelta
from AWS.common.custom_waiter import CustomWaiter, WaitState
logging.getLogger('boto').setLevel(logging.CRITICAL)
class KinesisAnalyticsApplicationUpdatedWaiter(CustomWaiter):
"""
Subclass of the base custom waiter class.
Wait for the Kinesis analytics application being updated to a specific status.
"""
def __init__(self, client: botocore.client, status: str):
"""
Initialize the waiter.
:param client: Boto3 client to use.
:param status: Expected status.
"""
super().__init__(
'KinesisAnalyticsApplicationUpdated',
'DescribeApplication',
'ApplicationDetail.ApplicationStatus',
{status: WaitState.SUCCESS},
client)
def wait(self, application_name: str):
"""
Wait for the expected status.
:param application_name: Name of the Kinesis analytics application.
"""
self._wait(ApplicationName=application_name)
class GlueCrawlerReadyWaiter(CustomWaiter):
"""
Subclass of the base custom waiter class.
Wait for the Glue crawler to finish its processing.
"""
def __init__(self, client: botocore.client):
"""
Initialize the waiter.
:param client: Boto3 client to use.
"""
super().__init__(
'GlueCrawlerReady',
'GetCrawler',
'Crawler.State',
{'READY': WaitState.SUCCESS},
client)
def wait(self, crawler_name):
"""
Wait for the expected status.
:param crawler_name: Name of the Glue crawler.
"""
self._wait(Name=crawler_name)
class DataLakeMetricsDeliveredWaiter(CustomWaiter):
"""
Subclass of the base custom waiter class.
Wait for the expected directory being created in the S3 bucket.
"""
def __init__(self, client: botocore.client):
"""
Initialize the waiter.
:param client: Boto3 client to use.
"""
super().__init__(
'DataLakeMetricsDelivered',
'ListObjectsV2',
'KeyCount > `0`',
{True: WaitState.SUCCESS},
client)
def wait(self, bucket_name, prefix):
"""
Wait for the expected directory being created.
:param bucket_name: Name of the S3 bucket.
:param prefix: Name of the expected directory prefix.
"""
self._wait(Bucket=bucket_name, Prefix=prefix)
class CloudWatchMetricsDeliveredWaiter(CustomWaiter):
"""
Subclass of the base custom waiter class.
Wait for the expected metrics being delivered to CloudWatch.
"""
def __init__(self, client: botocore.client):
"""
Initialize the waiter.
:param client: Boto3 client to use.
"""
super().__init__(
'CloudWatchMetricsDelivered',
'GetMetricStatistics',
'length(Datapoints) > `0`',
{True: WaitState.SUCCESS},
client)
def wait(self, namespace, metrics_name, dimensions, start_time):
"""
Wait for the expected metrics being delivered.
:param namespace: Namespace of the metrics.
:param metrics_name: Name of the metrics.
:param dimensions: Dimensions of the metrics.
:param start_time: Start time for generating the metrics.
"""
self._wait(
Namespace=namespace,
MetricName=metrics_name,
Dimensions=dimensions,
StartTime=start_time,
EndTime=start_time + timedelta(0, self.timeout),
Period=60,
Statistics=[
'SampleCount'
],
Unit='Count'
)

@ -16,12 +16,15 @@ import boto3
import ly_test_tools.environment.process_utils as process_utils
from typing import List
BOOTSTRAP_STACK_NAME = 'CDKToolkit'
BOOTSTRAP_STAGING_BUCKET_LOGIC_ID = 'StagingBucket'
class Cdk:
"""
Cdk class that provides methods to run cdk application commands.
Expects system to have NodeJS, AWS CLI and CDK installed globally and have their paths setup as env variables.
"""
def __init__(self, cdk_path: str, project: str, account_id: str,
workspace: pytest.fixture, session: boto3.session.Session):
"""
@ -49,12 +52,24 @@ class Cdk:
env=self._cdk_env,
shell=True)
def bootstrap(self) -> None:
"""
Deploy the bootstrap stack.
"""
bootstrap_cmd = ['cdk', 'bootstrap',
f'aws://{self._cdk_env["O3DE_AWS_DEPLOY_ACCOUNT"]}/{self._cdk_env["O3DE_AWS_DEPLOY_REGION"]}']
process_utils.check_call(
bootstrap_cmd,
cwd=self._cdk_path,
env=self._cdk_env,
shell=True)
def list(self) -> List[str]:
"""
lists cdk stack names
:return List of cdk stack names
"""
if not self._cdk_path:
return []
@ -123,6 +138,38 @@ class Cdk:
self._stacks = []
self._cdk_path = ''
@staticmethod
def remove_bootstrap_stack(aws_utils: pytest.fixture) -> None:
"""
Remove the CDK bootstrap stack.
:param aws_utils: aws_utils fixture.
"""
# Check if the bootstrap stack exists.
response = aws_utils.client('cloudformation').describe_stacks(
StackName=BOOTSTRAP_STACK_NAME
)
stacks = response.get('Stacks', [])
if not stacks:
return
# Clear the bootstrap staging bucket before deleting the bootstrap stack.
response = aws_utils.client('cloudformation').describe_stack_resource(
StackName=BOOTSTRAP_STACK_NAME,
LogicalResourceId=BOOTSTRAP_STAGING_BUCKET_LOGIC_ID
)
staging_bucket_name = response.get('StackResourceDetail', {}).get('PhysicalResourceId', '')
if staging_bucket_name:
s3 = aws_utils.resource('s3')
bucket = s3.Bucket(staging_bucket_name)
for key in bucket.objects.all():
key.delete()
# Delete the bootstrap stack.
aws_utils.client('cloudformation').delete_stack(
StackName=BOOTSTRAP_STACK_NAME
)
@pytest.fixture(scope='function')
def cdk(
@ -131,6 +178,7 @@ def cdk(
feature_name: str,
workspace: pytest.fixture,
aws_utils: pytest.fixture,
bootstrap_required: bool = True,
destroy_stacks_on_teardown: bool = True) -> Cdk:
"""
Fixture for setting up a Cdk
@ -140,6 +188,8 @@ def cdk(
:param feature_name: Feature gem name to expect cdk folder in.
:param workspace: ly_test_tools workspace fixture.
:param aws_utils: aws_utils fixture.
:param bootstrap_required: Whether the bootstrap stack needs to be created to
provision resources the AWS CDK needs to perform the deployment.
:param destroy_stacks_on_teardown: option to control calling destroy ot the end of test.
:return Cdk class object.
"""
@ -147,9 +197,14 @@ def cdk(
cdk_path = f'{workspace.paths.engine_root()}/Gems/{feature_name}/cdk'
cdk_obj = Cdk(cdk_path, project, aws_utils.assume_account_id(), workspace, aws_utils.assume_session())
if bootstrap_required:
cdk_obj.bootstrap()
def teardown():
if destroy_stacks_on_teardown:
cdk_obj.destroy()
cdk_obj.remove_bootstrap_stack(aws_utils)
request.addfinalizer(teardown)
return cdk_obj

@ -0,0 +1,134 @@
"""
All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
its licensors.
For complete copyright and license terms please see the LICENSE at the root of this
distribution (the "License"). All use of this software is governed by the License,
or, if provided, by the license below or the license accompanying this file. Do not
remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
"""
import boto3
import configparser
import logging
import os
import pytest
import typing
logger = logging.getLogger(__name__)
logging.getLogger('boto').setLevel(logging.CRITICAL)
class AwsCredentials:
def __init__(self, profile_name: str):
self._profile_name = profile_name
self._credentials_path = os.environ.get('AWS_SHARED_CREDENTIALS_FILE')
if not self._credentials_path:
# Home directory location varies based on the operating system, but is referred to using the environment
# variables %UserProfile% in Windows and $HOME or ~ (tilde) in Unix-based systems.
self._credentials_path = os.path.join(os.environ.get('UserProfile', os.path.expanduser('~')),
'.aws', 'credentials')
self._credentials_file_exists = os.path.exists(self._credentials_path)
self._credentials = configparser.ConfigParser()
self._credentials.read(self._credentials_path)
def get_aws_credentials(self) -> typing.Tuple[str, str, str]:
"""
Get aws credentials stored in the specific named profile.
:return AWS credentials.
"""
access_key_id = self._get_aws_credential_attribute_value('aws_access_key_id')
secret_access_key = self._get_aws_credential_attribute_value('aws_secret_access_key')
session_token = self._get_aws_credential_attribute_value('aws_session_token')
return access_key_id, secret_access_key, session_token
def set_aws_credentials_by_session(self, session: boto3.Session) -> None:
"""
Set AWS credentials stored in the specific named profile using an assumed role session.
:param session: assumed role session.
"""
credentials = session.get_credentials().get_frozen_credentials()
self.set_aws_credentials(credentials.access_key, credentials.secret_key, credentials.token)
def set_aws_credentials(self, aws_access_key_id: str, aws_secret_access_key: str,
aws_session_token: str) -> None:
"""
Set AWS credentials stored in the specific named profile.
:param aws_access_key_id: AWS access key id.
:param aws_secret_access_key: AWS secrete access key.
:param aws_session_token: AWS assumed role session.
"""
self._set_aws_credential_attribute_value('aws_access_key_id', aws_access_key_id)
self._set_aws_credential_attribute_value('aws_secret_access_key', aws_secret_access_key)
self._set_aws_credential_attribute_value('aws_session_token', aws_session_token)
if (len(self._credentials.sections()) == 0) and (not self._credentials_file_exists):
os.remove(self._credentials_path)
return
with open(self._credentials_path, 'w+') as credential_file:
self._credentials.write(credential_file)
def _get_aws_credential_attribute_value(self, attribute_name: str) -> str:
"""
Get the value of an AWS credential attribute stored in the specific named profile.
:param attribute_name: Name of the AWS credential attribute.
:return Value of the AWS credential attribute.
"""
try:
value = self._credentials.get(self._profile_name, attribute_name)
except configparser.NoSectionError:
# Named profile or key doesn't exist
value = None
except configparser.NoOptionError:
# Named profile doesn't have the specified attribute
value = None
return value
def _set_aws_credential_attribute_value(self, attribute_name: str, attribute_value: str) -> None:
"""
Set the value of an AWS credential attribute stored in the specific named profile.
:param attribute_name: Name of the AWS credential attribute.
:param attribute_value: Value of the AWS credential attribute.
"""
if self._profile_name not in self._credentials:
self._credentials[self._profile_name] = {}
if attribute_value is None:
self._credentials.remove_option(self._profile_name, attribute_name)
# Remove the named profile if it doesn't have any AWS credential attribute.
if len(self._credentials[self._profile_name]) == 0:
self._credentials.remove_section(self._profile_name)
else:
self._credentials[self._profile_name][attribute_name] = attribute_value
@pytest.fixture(scope='function')
def aws_credentials(request: pytest.fixture, aws_utils: pytest.fixture, profile_name: str):
"""
Fixture for setting up temporary AWS credentials from assume role.
:param request: _pytest.fixtures.SubRequest class that handles getting
a pytest fixture from a pytest function/fixture.
:param aws_utils: aws_utils fixture.
:param profile_name: Named AWS profile to store temporary credentials.
"""
aws_credentials_obj = AwsCredentials(profile_name)
original_access_key, original_secret_access_key, original_token = aws_credentials_obj.get_aws_credentials()
aws_credentials_obj.set_aws_credentials_by_session(aws_utils.assume_session())
def teardown():
# Reset to the named profile using the original AWS credentials
aws_credentials_obj.set_aws_credentials(original_access_key, original_secret_access_key, original_token)
request.addfinalizer(teardown)
return aws_credentials_obj

@ -1,82 +1,90 @@
"""
All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
its licensors.
For complete copyright and license terms please see the LICENSE at the root of this
distribution (the "License"). All use of this software is governed by the License,
or, if provided, by the license below or the license accompanying this file. Do not
remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
"""
import boto3
import pytest
import logging
logger = logging.getLogger(__name__)
class AwsUtils:
def __init__(self, arn: str, session_name: str, region_name: str):
local_session = boto3.Session(profile_name='default')
local_sts_client = local_session.client('sts')
self._local_account_id = local_sts_client.get_caller_identity()["Account"]
logger.info(f'Local Account Id: {self._local_account_id}')
response = local_sts_client.assume_role(RoleArn=arn, RoleSessionName=session_name)
self._assume_session = boto3.Session(aws_access_key_id=response['Credentials']['AccessKeyId'],
aws_secret_access_key=response['Credentials']['SecretAccessKey'],
aws_session_token=response['Credentials']['SessionToken'],
region_name=region_name)
assume_sts_client = self._assume_session.client('sts')
assume_account_id = assume_sts_client.get_caller_identity()["Account"]
logger.info(f'Assume Account Id: {assume_account_id}')
self._assume_account_id = assume_account_id
def client(self, service: str):
"""
Get the client for a specific AWS service from configured session
:return: Client for the AWS service.
"""
return self._assume_session.client(service)
def assume_session(self):
return self._assume_session
def local_account_id(self):
return self._local_account_id
def assume_account_id(self):
return self._assume_account_id
def destroy(self) -> None:
"""
clears stored session
"""
self._assume_session = None
@pytest.fixture(scope='function')
def aws_utils(
request: pytest.fixture,
assume_role_arn: str,
session_name: str,
region_name: str):
"""
Fixture for setting up a Cdk
:param request: _pytest.fixtures.SubRequest class that handles getting
a pytest fixture from a pytest function/fixture.
:param assume_role_arn: Role used to fetch temporary aws credentials, configure service clients with obtained credentials.
:param session_name: Session name to set.
:param region_name: AWS account region to set for session.
:return AWSUtils class object.
"""
aws_utils_obj = AwsUtils(assume_role_arn, session_name, region_name)
def teardown():
aws_utils_obj.destroy()
request.addfinalizer(teardown)
return aws_utils_obj
"""
All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
its licensors.
For complete copyright and license terms please see the LICENSE at the root of this
distribution (the "License"). All use of this software is governed by the License,
or, if provided, by the license below or the license accompanying this file. Do not
remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
"""
import boto3
import pytest
import logging
logger = logging.getLogger(__name__)
logging.getLogger('boto').setLevel(logging.CRITICAL)
class AwsUtils:
def __init__(self, arn: str, session_name: str, region_name: str):
local_session = boto3.Session(profile_name='default')
local_sts_client = local_session.client('sts')
self._local_account_id = local_sts_client.get_caller_identity()["Account"]
logger.info(f'Local Account Id: {self._local_account_id}')
response = local_sts_client.assume_role(RoleArn=arn, RoleSessionName=session_name)
self._assume_session = boto3.Session(aws_access_key_id=response['Credentials']['AccessKeyId'],
aws_secret_access_key=response['Credentials']['SecretAccessKey'],
aws_session_token=response['Credentials']['SessionToken'],
region_name=region_name)
assume_sts_client = self._assume_session.client('sts')
assume_account_id = assume_sts_client.get_caller_identity()["Account"]
logger.info(f'Assume Account Id: {assume_account_id}')
self._assume_account_id = assume_account_id
def client(self, service: str):
"""
Get the client for a specific AWS service from configured session
:return: Client for the AWS service.
"""
return self._assume_session.client(service)
def resource(self, service: str):
"""
Get the resource for a specific AWS service from configured session
:return: Client for the AWS service.
"""
return self._assume_session.resource(service)
def assume_session(self):
return self._assume_session
def local_account_id(self):
return self._local_account_id
def assume_account_id(self):
return self._assume_account_id
def destroy(self) -> None:
"""
clears stored session
"""
self._assume_session = None
@pytest.fixture(scope='function')
def aws_utils(
request: pytest.fixture,
assume_role_arn: str,
session_name: str,
region_name: str):
"""
Fixture for AWS util functions
:param request: _pytest.fixtures.SubRequest class that handles getting
a pytest fixture from a pytest function/fixture.
:param assume_role_arn: Role used to fetch temporary aws credentials, configure service clients with obtained credentials.
:param session_name: Session name to set.
:param region_name: AWS account region to set for session.
:return AWSUtils class object.
"""
aws_utils_obj = AwsUtils(assume_role_arn, session_name, region_name)
def teardown():
aws_utils_obj.destroy()
request.addfinalizer(teardown)
return aws_utils_obj

@ -0,0 +1,91 @@
"""
All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
its licensors.
For complete copyright and license terms please see the LICENSE at the root of this
distribution (the "License"). All use of this software is governed by the License,
or, if provided, by the license below or the license accompanying this file. Do not
remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
"""
from enum import Enum
import botocore.client
import botocore.waiter
import logging
logging.getLogger('boto').setLevel(logging.CRITICAL)
class WaitState(Enum):
SUCCESS = 'success'
FAILURE = 'failure'
class CustomWaiter:
"""
Base class for a custom waiter.
Modified from:
https://docs.aws.amazon.com/code-samples/latest/catalog/python-demo_tools-custom_waiter.py.html
"""
def __init__(
self, name: str, operation: str, argument: str,
acceptors: dict, client: botocore.client, delay: int = 30, max_tries: int = 10,
matcher='path'):
"""
Subclasses should pass specific operations, arguments, and acceptors to
their superclass.
:param name: The name of the waiter. This can be any descriptive string.
:param operation: The operation to wait for. This must match the casing of
the underlying operation model, which is typically in
CamelCase.
:param argument: The dict keys used to access the result of the operation, in
dot notation. For example, 'Job.Status' will access
result['Job']['Status'].
:param acceptors: The list of acceptors that indicate the wait is over. These
can indicate either success or failure. The acceptor values
are compared to the result of the operation after the
argument keys are applied.
:param client: The Boto3 client.
:param delay: The number of seconds to wait between each call to the operation. Default to 30 seconds.
:param max_tries: The maximum number of tries before exiting. Default to 10.
:param matcher: The kind of matcher to use. Default to 'path'.
"""
self.name = name
self.operation = operation
self.argument = argument
self.client = client
self.waiter_model = botocore.waiter.WaiterModel({
'version': 2,
'waiters': {
name: {
"delay": delay,
"operation": operation,
"maxAttempts": max_tries,
"acceptors": [{
"state": state.value,
"matcher": matcher,
"argument": argument,
"expected": expected
} for expected, state in acceptors.items()]
}}})
self.waiter = botocore.waiter.create_waiter_with_client(
self.name, self.waiter_model, self.client)
self._timeout = delay * max_tries
def _wait(self, **kwargs):
"""
Starts the botocore wait loop.
:param kwargs: Keyword arguments that are passed to the operation being polled.
"""
self.waiter.wait(**kwargs)
@property
def timeout(self):
return self._timeout

@ -128,16 +128,5 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED AND PAL_TRAIT_BUILD_HOST_TOOLS)
RUNTIME_DEPENDENCIES
AZ::AssetProcessorBatch
)
# Need performance improvements LYN-1218
# ly_add_pytest(
# NAME AssetPipelineTests.AssetRelocator
# PATH ${CMAKE_CURRENT_LIST_DIR}/asset_relocator_tests.py
# EXCLUDE_TEST_RUN_TARGET_FROM_IDE
# TEST_SUITE periodic
# TEST_SERIAL
# RUNTIME_DEPENDENCIES
# AZ::AssetProcessorBatch
# )
endif()

@ -64,6 +64,15 @@ class TestsAssetBuilder_WindowsAndMac(object):
):
"""
Verifying -debug parameter for AssetBuilder
Test Steps:
1. Create temporary workspace
2. Launch Asset Processor GUI
3. Add test assets to workspace
4. Run Asset Builder with debug on an intact slice
5. Check Asset Builder didn't fail to build
6. Run Asset Builder with debug on a corrupted slice
7. Verify corrupted slice produced an error
"""
env = ap_setup_fixture
intact_slice_failed = False

@ -80,6 +80,8 @@ class TestsAssetBundlerBatch_WindowsAndMac(object):
def test_WindowsAndMac_RunHelpCmd_ZeroExitCode(self, workspace, bundler_batch_helper):
"""
Simple calls to all AssetBundlerBatch --help to make sure a non-zero exit codes are returned.
Test will call each Asset Bundler Batch sub-command with help and will error on a non-0 exit code
"""
bundler_batch_helper.call_bundlerbatch(help="")
bundler_batch_helper.call_seeds(help="")
@ -98,6 +100,12 @@ class TestsAssetBundlerBatch_WindowsAndMac(object):
r"""
Tests that an asset list created maps dependencies correctly.
testdependencieslevel\level.pak and lists of known dependencies are used for validation
Test Steps:
1. Create an asset list from the level.pak
2. Create Lists of expected assets in the level.pak
3. Add lists of expected assets to a single list
4. Compare list of expected assets to actual assets
"""
helper = bundler_batch_helper
@ -300,6 +308,15 @@ class TestsAssetBundlerBatch_WindowsAndMac(object):
"""
Validates destructive overwriting for asset lists and
that generating debug information does not affect asset list creation
1. Create an asset list from seed_list
2. Validate asset list was created
3. Read and store contents of asset list into memory
4. Attempt to create a new asset list in without using --allowOverwrites
5. Verify that Asset Bundler returns false
6. Verify that file contents of the orignally created asset list did not change from what was stored in memory
7. Attempt to create a new asset list without debug while allowing overwrites
8. Verify that file contents of the orignally created asset list changed from what was stored in memory
"""
helper = bundler_batch_helper
seed_list = os.path.join(workspace.paths.engine_root(), "Assets", "Engine", "SeedAssetList.seed") # Engine seed list
@ -375,6 +392,14 @@ class TestsAssetBundlerBatch_WindowsAndMac(object):
"""
Validates bundle creation both through the 'bundles' and 'bundlesettings'
subcommands.
Test Steps:
1. Create an asset list
2. Create a bundle with the asset list and without a bundle settings file
3. Create a bundle with the asset list and a bundle settings file
4. Validate calling bundle doesn't perform destructive overwrite without --allowOverwrites
5. Calling bundle again with --alowOverwrites performs destructive overwrite
6. Validate contents of original bundle and overwritten bundle
"""
helper = bundler_batch_helper
seed_list = os.path.join(workspace.paths.engine_root(), "Assets", "Engine", "SeedAssetList.seed") # Engine seed list
@ -457,6 +482,16 @@ class TestsAssetBundlerBatch_WindowsAndMac(object):
"""
Creates bundles using the same asset list and compares that they are created equally. Also
validates that platform bundles exclude/include an expected file. (excluded for WIN, included for MAC)
Test Steps:
1. Create an asset list
2. Create bundles for both PC & Mac
3. Validate that bundles were created
4. Verify that expected missing file is not in windows bundle
5. Verify that expected file is in the mac bundle
6. Create duplicate bundles with allowOverwrites
7. Verify that files were generated
8. Verify original bundle checksums are equal to new bundle checksums
"""
helper = bundler_batch_helper
# fmt:off
@ -571,6 +606,24 @@ class TestsAssetBundlerBatch_WindowsAndMac(object):
"""
Validates that the 'seeds' subcommand can add and remove seeds and seed platforms properly.
Also checks that destructive overwrites require the --allowOverwrites flag
Test Steps:
1. Create a PC Seed List from a test asset
2. Validate that seed list was generated with proper platform flag
3. Add Mac & PC as platforms to the seed list
4. Verify that seed has both Mac & PC platform flags
5. Remove Mac as a platform from the seed list
6. Verify that seed only has PC as a platform flag
7. Attempt to add a platform without using the --platform argument
8. Verify that asset bundler returns False and file contents did not change
9. Add Mac platform via --addPlatformToSeeds
10. Validate that seed has both Mac & PC platform flags
11. Attempt to remove platform without specifying a platform
12. Validate that seed has both Mac & PC platform flags
13. Validate that seed list contents did not change
14. Remove seed
15. Validate that seed was removed from the seed list
"""
helper = bundler_batch_helper
@ -692,6 +745,12 @@ class TestsAssetBundlerBatch_WindowsAndMac(object):
"""
Tests asset list comparison, both by file and by comparison type. Uses a set
of controlled test assets to compare resulting output asset lists
1. Create comparison rules files
2. Create seed files for different sets of test assets
3. Create assetlist files for seed files
4. Validate assetlists were created properly
5. Compare using comparison rules files and just command line arguments
"""
helper = bundler_batch_helper
env = ap_setup_fixture
@ -1021,6 +1080,16 @@ class TestsAssetBundlerBatch_WindowsAndMac(object):
"""
Tests that assetlists are created equivalent to the output while being created, and
makes sure overwriting an existing file without the --allowOverwrites fails
Test Steps:
1. Check that Asset List creation requires PC platform flag
2. Create a PC Asset List using asset info file and default seed lists using --print
3. Validate all assets output are present in the asset list
4. Create a seed file
5. Attempt to overwrite Asset List without using --allowOverwrites
6. Validate that command returned an error and file contents did not change
7. Specifying platform but not "add" or "remove" should fail
8. Verify file Has changed
"""
helper = bundler_batch_helper
@ -1102,7 +1171,16 @@ class TestsAssetBundlerBatch_WindowsAndMac(object):
def test_WindowsAndMac_AP_BundleProcessing_BundleProcessedAtRuntime(self, workspace, bundler_batch_helper,
asset_processor, request):
# fmt:on
"""Test to make sure the AP GUI will process a newly created bundle file"""
"""
Test to make sure the AP GUI will process a newly created bundle file
Test Steps:
1. Make asset list file (used for bundle creation)
2. Start Asset Processor GUI
3. Make bundle in <project_folder>/Bundles
4. Validate file was created in Bundles folder
5. Make sure bundle now exists in cache
"""
# Set up helpers and variables
helper = bundler_batch_helper
@ -1131,6 +1209,8 @@ class TestsAssetBundlerBatch_WindowsAndMac(object):
addSeed=level_pak,
assetListFile=helper["asset_info_file_request"],
)
# Run Asset Processor GUI
result, _ = asset_processor.gui_process()
assert result, "AP GUI failed"
@ -1155,6 +1235,12 @@ class TestsAssetBundlerBatch_WindowsAndMac(object):
@pytest.mark.assetpipeline
# fmt:off
def test_WindowsAndMac_FilesMarkedSkip_FilesAreSkipped(self, workspace, bundler_batch_helper):
"""
Test Steps:
1. Create an asset list with a file marked as skip
2. Verify file was created
3. Verify that only the expected assets are present in the created asset list
"""
expected_assets = [
"ui/canvases/lyshineexamples/animation/multiplesequences.uicanvas",
"ui/textures/prefab/button_normal.sprite"
@ -1178,6 +1264,12 @@ class TestsAssetBundlerBatch_WindowsAndMac(object):
# fmt:off
def test_WindowsAndMac_AssetListSkipOneOfTwoParents_SharedDependencyIsIncluded(self, workspace,
bundler_batch_helper):
"""
Test Steps:
1. Create Asset List with a parent asset that is skipped
2. Verify that Asset List was created
3. Verify that only the expected assets are present in the asset list
"""
expected_assets = [
"testassets/bundlerskiptest_grandparent.dynamicslice",
"testassets/bundlerskiptest_parenta.dynamicslice",
@ -1206,6 +1298,13 @@ class TestsAssetBundlerBatch_WindowsAndMac(object):
@pytest.mark.assetpipeline
# fmt:off
def test_WindowsAndMac_AssetLists_SkipRoot_ExcludesAll(self, workspace, bundler_batch_helper):
"""
Negative scenario test that skips the same file being used as the parent seed.
Test Steps:
1. Create an asset list that skips the root asset
2. Verify that asset list was not generated
"""
result, _ = bundler_batch_helper.call_assetLists(
assetListFile=bundler_batch_helper['asset_info_file_request'],
@ -1222,6 +1321,13 @@ class TestsAssetBundlerBatch_WindowsAndMac(object):
@pytest.mark.assetpipeline
# fmt:off
def test_WindowsAndMac_AssetLists_SkipUniversalWildcard_ExcludesAll(self, workspace, bundler_batch_helper):
"""
Negative scenario test that uses the all wildcard when generating an asset list.
Test Steps:
1. Create an Asset List while using the universal all wildcard "*"
2. Verify that asset list was not generated
"""
result, _ = bundler_batch_helper.call_assetLists(
assetListFile=bundler_batch_helper['asset_info_file_request'],

@ -67,7 +67,19 @@ class TestsAssetProcessorBatch_DependenycyTests(object):
libs/materialeffects/surfacetypes.xml is listed as an entry engine_dependencies.xml
libs/materialeffects/surfacetypes.xml is not listed as a missing dependency
in the 'assetprocessorbatch' console output
Test Steps:
1. Assets are pre-processed
2. Verify that engine_dependencies.xml exists
3. Verify engine_dependencies.xml has surfacetypes.xml present
4. Run Missing Dependency scanner against the engine_dependenciese.xml
5. Verify that Surfacetypes.xml is NOT in the missing depdencies output
6. Add the schema file which allows our xml parser to understand dependencies for our engine_dependencies file
7. Process assets
8. Run Missing Dependency scanner against the engine_dependenciese.xml
9. Verify that surfacetypes.xml is in the missing dependencies out
"""
env = ap_setup_fixture
BATCH_LOG_PATH = env["ap_batch_log_file"]
asset_processor.create_temp_asset_root()
@ -137,6 +149,11 @@ class TestsAssetProcessorBatch_DependenycyTests(object):
def test_WindowsMacPlatforms_BatchCheckSchema_ValidateErrorChecking(self, workspace, asset_processor,
ap_setup_fixture, folder, schema):
# fmt:on
"""
Test Steps:
1. Run the Missing Dependency Scanner against everything
2. Verify that there are no missing dependencies.
"""
env = ap_setup_fixture
def missing_dependency_log_lines(log) -> [str]:

@ -60,6 +60,15 @@ class TestsAssetProcessorBatch_DependenycyTests(object):
Verify that Schemas can be loaded via Gems utilizing the fonts schema
:returns: None
Test Steps:
1. Run Missing Dependency Scanner against %fonts%.xml when no fonts are present
2. Verify fonts are scanned
3. Verify that missing dependencies are found for fonts
4. Add fonts to game project
5. Run Missing Dependency Scanner against %fonts%.xml when fonts are present
6. Verify that same amount of fonts are scanned
7. Verify that there are no missing dependencies.
"""
schema_name = "Font.xmlschema"
asset_processor.create_temp_asset_root()

@ -100,6 +100,14 @@ class TestsAssetProcessorBatch_AllPlatforms(object):
@pytest.mark.BAT
@pytest.mark.assetpipeline
def test_RunAPBatch_TwoPlatforms_ExitCodeZero(self, asset_processor):
"""
Tests Process assets for PC & Mac and verifies that processing exited without error
Test Steps:
1. Add Mac and PC as enabled platforms
2. Process Assets
3. Validate that AP exited cleanly
"""
asset_processor.create_temp_asset_root()
asset_processor.enable_asset_processor_platform("pc")
asset_processor.enable_asset_processor_platform("mac")
@ -111,6 +119,14 @@ class TestsAssetProcessorBatch_AllPlatforms(object):
@pytest.mark.assetpipeline
@pytest.mark.test_case_id('C1571826')
def test_RunAPBatch_OnlyIncludeInvalidAssets_NoAssetsAdded(self, asset_processor, ap_setup_fixture):
"""
Tests processing invalid assets and validating that no assets were moved to the cache
Test Steps:
1. Create a test environment with invalid assets
2. Run asset processor
3. Validate that no assets were found in the cache
"""
asset_processor.prepare_test_environment(ap_setup_fixture["tests_dir"], "test_ProcessAssets_OnlyIncludeInvalidAssets_NoAssetsAdded")
result, _ = asset_processor.batch_process()
@ -127,6 +143,16 @@ class TestsAssetProcessorBatch_AllPlatforms(object):
"recognized as failing in the logs. There appears to be a window where the AutoFailJob doesn't complete"
"before the shutdown completes and the failure doesn't end up counting")
def test_ProcessAssets_IncludeTwoAssetsWithSameProduct_FailingOnSecondAsset(self, asset_processor, ap_setup_fixture):
"""
Tests processing two source assets with the same product file and validates that the second source will error
Test Steps:
1. Create a test environment that has two source files with the same product
2. Run asset processor
3. Validate that 1 asset failed to process
4. Validate that only one product file with the expected name is found in the cache
"""
asset_processor.prepare_test_environment(ap_setup_fixture["tests_dir"], "test_ProcessAssets_IncludeTwoAssetsWithSameProduct_FailingOnSecondAsset")
result, output = asset_processor.batch_process(capture_output = True, expect_failure = True)
@ -143,6 +169,17 @@ class TestsAssetProcessorBatch_AllPlatforms(object):
@pytest.mark.assetpipeline
@pytest.mark.test_case_id('C1587615')
def test_ProcessAndDeleteCache_APBatchShouldReprocess(self, asset_processor, ap_setup_fixture):
"""
Tests processing once, deleting the generated cache, then processing again and validates the cache is created
Test Steps:
1. Run asset processor
2. Compare the cache with expected output
3. Delete Cache
4. Compare the cache with expected output to verify that cache is gone
5. Run asset processor with fastscan disabled
6. Compare the cache with expected output
"""
# Deleting assets from Cache will make them re-processed in AP (after start)
# Copying test assets to project folder and deleting them from cache to make sure APBatch will process them
@ -174,6 +211,18 @@ class TestsAssetProcessorBatch_AllPlatforms(object):
@pytest.mark.assetpipeline
@pytest.mark.test_case_id('C1591564')
def test_ProcessAndChangeSource_APBatchShouldReprocess(self, asset_processor, ap_setup_fixture):
"""
Tests reprocessing of a modified asset and verifies that it was reprocessed
Test Steps:
1. Prepare test environment and copy test asset over
2. Run asset processor
3. Verify asset processed
4. Verify asset is in cache
4. Modify asset
5. Re-run asset processor
6. Verify asset was processed
"""
# AP Batch Processing changed files (after start)
# Copying test assets to project folder and deleting them from cache to make sure APBatch will process them
@ -208,6 +257,18 @@ class TestsAssetProcessorBatch_AllPlatforms(object):
@pytest.mark.BAT
@pytest.mark.assetpipeline
def test_ProcessByBothApAndBatch_Md5ShouldMatch(self, asset_processor, ap_setup_fixture):
"""
Tests that a cache generated by AP GUI is the same as AP Batch
Test Steps:
1. Create test environment with test assets
2. Call asset processor batch
3. Get checksum for file cache
4. Clean up test environment
5. Call asset processor gui with quitonidle
6. Get checksum for file cache
7. Verify that checksums are equal
"""
# AP Batch and AP app processed assets MD5 sums should be the same
# Copying test assets to project folder and deleting them from cache to make sure APBatch will process them
@ -240,6 +301,16 @@ class TestsAssetProcessorBatch_AllPlatforms(object):
@pytest.mark.assetpipeline
@pytest.mark.test_case_id('C1612446')
def test_AddSameAssetsDifferentNames_ShouldProcess(self, asset_processor, ap_setup_fixture):
"""
Tests Asset Processing of duplicate assets with different names and verifies that both assets are processed
Test Steps:
1. Create test environment with two identical source assets with different names
2. Run asset processor
3. Verify that assets didn't fail to process
4. Verify the correct number of jobs were performed
5. Verify that product files are in the cache
"""
# Feed two similar slices and texture with different names - should process without any issues
# Copying test assets to project folder and deleting them from cache to make sure APBatch will process them
@ -277,6 +348,19 @@ class TestsAssetProcessorBatch_AllPlatforms(object):
"recognized as failing in the logs. There appears to be a window where the AutoFailJob doesn't complete"
"before the shutdown completes and the failure doesn't end up counting")
def test_AddTwoTexturesWithSameName_ShouldProcessAfterRename(self, asset_processor, ap_setup_fixture):
"""
Tests processing of two textures with the same name then verifies that AP will successfully process after
renaming one of the textures
Test Steps:
1. Create test environment with two textures that have the same name
2. Launch Asset Processor
3. Validate that Asset Processor generates an error
4. Rename texture files
5. Run asset processor
6. Verify that asset processor does not error
7. Verify that expected product files are in the cache
"""
# Feed two different textures with same name (but different extensions) - ap will fail
# Rename one of textures and failure should go away
@ -312,6 +396,15 @@ class TestsAssetProcessorBatch_AllPlatforms(object):
@pytest.mark.BAT
@pytest.mark.assetpipeline
def test_InvalidServerAddress_Warning_Logs(self, asset_processor):
"""
Tests running Asset Processor with an invalid server address and verifies that AP returns a warning about
an invalid server address
Test Steps:
1. Launch asset processor while providing an invalid server address
2. Verify asset processor does not fail
3. Verify that asset processor generated a warning informing the user about an invalid server address
"""
asset_processor.create_temp_asset_root()
# Launching AP and making sure that the warning exists
@ -327,6 +420,12 @@ class TestsAssetProcessorBatch_AllPlatforms(object):
def test_AllSupportedPlatforms_IncludeValidAssets_AssetsProcessed(self, asset_processor, ap_setup_fixture):
"""
AssetProcessorBatch is successfully processing newly added assets
Test Steps:
1. Create a test environment with test assets
2. Launch Asset Processor
3. Verify that asset processor does not fail to process
4. Verify assets are not missing from the cache
"""
env = ap_setup_fixture
@ -350,6 +449,14 @@ class TestsAssetProcessorBatch_AllPlatforms(object):
def test_AllSupportedPlatforms_DeletedAssets_DeletedFromCache(self, asset_processor, ap_setup_fixture):
"""
AssetProcessor successfully deletes cached items when removed from project
Test Steps:
1. Create a test environment with test assets
2. Run asset processor
3. Verify expected assets are in the cache
4. Delete test assets
5. Run asset processor
6. Verify expected assets are in the cache
"""
env = ap_setup_fixture
@ -385,6 +492,10 @@ class TestsAssetProcessorBatch_AllPlatforms(object):
"""
Tests that when cache is deleted (no cache) and AssetProcessorBatch runs,
it successfully starts and processes assets.
Test Steps:
1. Run asset processor
2. Verify asset processor exits cleanly
"""
asset_processor.create_temp_asset_root()
@ -402,6 +513,14 @@ class TestsAssetProcessorBatch_AllPlatforms(object):
# fmt:on
"""
AssetProcessor successfully recovers assets from cache when deleted.
Test Steps:
1. Create test enviornment with test assets
2. Run Asset Processor and verify it exits cleanly
3. Make sure cache folder was generated
4. Delete temp cache assets but leave database behind
5. Run asset processor and verify it exits cleanly
6. Verify expected files were generated in the cache
"""
env = ap_setup_fixture
@ -434,6 +553,14 @@ class TestsAssetProcessorBatch_AllPlatforms(object):
@pytest.mark.assetpipeline
# fmt:off
def test_AllSupportedPlatforms_RunFastScanOnEmptyCache_FullScanRuns(self, ap_setup_fixture, asset_processor):
"""
Tests fast scan processing on an empty cache and verifies that a full analyis will be peformed
Test Steps:
1. Create a test environment
2. Execute asset processor batch with fast scan enabled
3. Verify that a full analysis is performed
"""
# fmt:on
env = ap_setup_fixture
asset_processor.create_temp_asset_root()
@ -455,6 +582,11 @@ class TestsAssetProcessorBatch_AllPlatforms(object):
"""
After running the APBatch and AP GUI, Logs directory should exist (C1564055),
JobLogs, Batch log, and GUI log should exist in the logs directory (C1564056)
Test Steps:
1. Run asset processor batch
2. Run asset processor gui with quit on idle
3. Verify that logs exist for both AP Batch & AP GUI
"""
asset_processor.create_temp_asset_root()
LOG_PATH = {
@ -536,6 +668,11 @@ class TestsAssetProcessorBatch_AllPlatforms(object):
"""
Utilizing corrupted test assets, run the batch process to verify the
AP logs the failure to process the corrupted file.
Test Steps:
1. Create test environment with corrupted slice
2. Launch Asset Processor
3. Verify that asset processor fails to process corrupted slice
"""
env = ap_setup_fixture
error_line_found = False
@ -552,6 +689,15 @@ class TestsAssetProcessorBatch_AllPlatforms(object):
@pytest.mark.BAT
@pytest.mark.assetpipeline
def test_validateDirectPreloadDependency_Found(self, asset_processor, ap_setup_fixture, workspace):
"""
Tests processing an asset with a circular dependency and verifies that Asset Processor will return an error
notifying the user about a circular dependency.
Test Steps:
1. Create test environment with an asset that has a circular dependency
2. Launch asset processor
3. Verify that error is returned informing the user that the asset has a circular dependency
"""
env = ap_setup_fixture
error_line_found = False
@ -567,6 +713,15 @@ class TestsAssetProcessorBatch_AllPlatforms(object):
@pytest.mark.BAT
@pytest.mark.assetpipeline
def test_validateNestedPreloadDependency_Found(self, asset_processor, ap_setup_fixture, workspace):
"""
Tests processing of a nested circular dependency and verifies that Asset Processor will return an error
notifying the user about a circular depdency
Test Steps:
1. Create test environment with an asset that has a nested circular dependency
2. Launch asset processor
3. Verify that error is returned informing the user that the asset has a circular dependency
"""
env = ap_setup_fixture
error_line_found = False

@ -80,6 +80,15 @@ class TestsAssetProcessorBatch_AllPlatforms(object):
# fmt:on
"""
Tests that fast scan mode can be used and is faster than full scan mode.
Test Steps:
1. Ensure all assets are processed
2. Run Asset Processor without fast scan and measure the time it takes to run
3. Capture Full Analysis was performed and number of assets processed
4. Run Asset Processor with full scan and measure the time it takes to run
5. Capture Full Analysis wans't performed and number of assets processed
6. Verify that fast scan was faster than full scan
7. Verify that full scan scanned more assets
"""
asset_processor.create_temp_asset_root()
@ -111,76 +120,23 @@ class TestsAssetProcessorBatch_AllPlatforms(object):
assert full_scan_time > fast_scan_time, "Fast scan was slower that full scan"
assert full_scan_analysis[0] > fast_scan_analysis[0], "Full scan did not process more assets than fast scan"
@pytest.mark.test_case_id("C18787404")
@pytest.mark.BAT
@pytest.mark.assetpipeline
@pytest.mark.skip(reason="External project is currently broken.") # LY-119863
def test_AllSupportedPlatforms_ExternalProject_APRuns(self, workspace, ap_external_project_setup_fixture):
external_resources = ap_external_project_setup_fixture
logger.info(f"Running external project test at path {external_resources['project_dir']}")
# Delete existing "external project" build if it exists
if os.path.exists(external_resources["project_dir"]):
fs.delete([external_resources["project_dir"]], True, True)
# fmt:off
assert not os.path.exists(external_resources["project_dir"]), \
f'{external_resources["project_dir"]} was not deleted'
# fmt:on
lmbr_cmd = [
workspace.paths.lmbr(),
"projects",
"create",
external_resources["project_name"],
"--template",
"EmptyTemplate",
"--app-root",
external_resources["project_dir"],
]
logger.info(f"Running lmbr projects create command '{lmbr_cmd}'")
try:
subprocess.check_call(lmbr_cmd)
except subprocess.CalledProcessError as e:
assert False, f"lmbr projects create failed\n{e.stderr}"
logger.info("...lmbr finished")
assert os.path.exists(external_resources["project_dir"]), "Project folder was not created"
# AssetProcessor for new External project. Uses mock workspace to emulate external project workspace
external_ap = AssetProcessor(external_resources["external_workspace"])
# fmt:off
assert external_ap.batch_process(fastscan=False), \
"Asset Processor Batch failed on external project"
# fmt:on
# Parse log looking for errors or failures
log = APLogParser(workspace.paths.ap_batch_log())
failures, errors = log.runs[-1]["Failures"], log.runs[-1]["Errors"]
assert failures == 0, f"There were {failures} asset processing failures"
assert errors == 0, f"There were {errors} asset processing errors"
# Check that project cache was created (DNE until AP makes it)
project_cache = os.path.join(external_resources["project_dir"], "Cache")
assert os.path.exists(project_cache), f"{project_cache} was not created by AP"
# Clean up external project
fs.delete([external_resources["project_dir"]], True, True)
# fmt:off
assert not os.path.exists(external_resources["project_dir"]), \
f"{external_resources['project_dir']} was not deleted"
# fmt:on
@pytest.mark.test_case_id("C4874121")
@pytest.mark.BAT
@pytest.mark.assetpipeline
@pytest.mark.parametrize("clear_type", ["rewrite", "delete_asset", "delete_dir"])
def test_AllSupportedPlatforms_DeleteBadAssets_BatchFailedJobsCleared(
self, workspace, request, ap_setup_fixture, asset_processor, clear_type):
"""
Tests the ability of Asset Processor to recover from processing of bad assets by removing them from scan folder
Test Steps:
1. Create testing environment with good and multiple bad assets
2. Run Asset Processor
3. Verify that bad assets fail to process
4. Fix a bad asset & delete the others
5. Run Asset Processor
6. Verify Asset Processor does not have any asset failues
"""
env = ap_setup_fixture
error_search_terms = ["WWWWWWWWWWWW"]
@ -250,6 +206,14 @@ class TestsAssetProcessorBatch_Windows(object):
Verify the AP batch and Gui can run and process assets independent of the Editor
We do not want or need to kill running Editors here as they can be involved in other tests
or simply being run locally in this branch or another
Test Steps:
1. Create temporary testing environment
2. Run asset processor GUI
3. Verify AP GUI doesn't error
4. Stop AP GUI
5. Run Asset Processor Batch with Fast Scan
5. Verify Asset Processor Batch exits cleanly
"""
asset_processor.create_temp_asset_root()
@ -272,6 +236,11 @@ class TestsAssetProcessorBatch_Windows(object):
"""
Request a run for an invalid platform
"AssetProcessor: Error: Platform in config file or command line 'notaplatform'" should be present in the logs
Test Steps:
1. Create temporary testing environment
2. Run Asset Processor with an invalid platform
3. Check that asset processor returns an Error notifying the user that the invalid platform is not supported
"""
asset_processor.create_temp_asset_root()
error_search_terms = 'AssetProcessor: Error: The list of enabled platforms in the settings registry does not contain platform ' \

@ -77,6 +77,13 @@ class TestsAssetProcessorGUI_Windows(object):
def test_SendInputOnControlChannel_ReceivedAndResponded(self, asset_processor):
"""
Test that the control channel connects and that communication works both directions
Test Steps:
1. Start Asset Processor
2. Send a Ping message to Asset Processor
3. Listen for Asset Processor response
4. Verify Asset Processor responds
5. Stop asset Processor
"""
asset_processor.create_temp_asset_root()
@ -129,7 +136,15 @@ class TestsAssetProcessorGUI_Windows(object):
# fmt:on
"""
Asset Processor Deletes processed assets when source is removed from project folder (while running)
Test Steps:
1. Create a temporary test environment
2. Run Asset Processor GUI set to stay open on idle and verify that it does not fail
3. Verify that assets were copied to the cache
4. Delete the source test asset directory
5. Verify assets are deleted from the cache
"""
env = ap_setup_fixture
# Copy test assets to project folder and verify test assets folder exists
@ -170,7 +185,18 @@ class TestsAssetProcessorGUI_Windows(object):
# fmt:on
"""
Processing changed files (while running)
Test Steps:
1. Create temporary test environment with test assets
2. Open Asset Processor GUI with set to stay open after idle and verify it does not fail
3. Verify contents of source asset for later comparison
4. Verify contents of product asset for later comparison
5. Modify contents of source asset
6. Wait for Asset Processor to go back to idle state
7. Verify contents of source asset are the modified version
8. Verify contents of product asset are the modified version
"""
env = ap_setup_fixture
# Copy test assets to project folder and verify test assets folder exists
@ -184,7 +210,7 @@ class TestsAssetProcessorGUI_Windows(object):
result, _ = asset_processor.gui_process(quitonidle=False)
assert result, "AP GUI failed"
# Verify contents of test asset in project folder before modication
# Verify contents of test asset in project folder before modification
with open(project_asset_path, "r") as project_asset_file:
assert project_asset_file.read() == "before_state"
@ -217,7 +243,14 @@ class TestsAssetProcessorGUI_Windows(object):
def test_WindowsPlatforms_RunAP_ProcessesIdle(self, asset_processor):
"""
Asset Processor goes idle
Test Steps:
1. Create a temporary testing evnironment
2. Run Asset Processor GUI without quitonidle
3. Verify AP Goes Idle
4. Verify AP goes below 1% CPU usage
"""
CPU_USAGE_THRESHOLD = 1.0 # CPU usage percentage delimiting idle from active
CPU_USAGE_WIND_DOWN = 10 # Time allowed in seconds for idle processes to stop using CPU
@ -245,7 +278,16 @@ class TestsAssetProcessorGUI_Windows(object):
):
"""
Processing newly added files to project folder (while running)
Test Steps:
1. Create a temporary testing environment with test assets
2. Create a secondary set of testing assets that have not been copied into the the testing environment
3. Start Asset Processor without quitonidle
4. While Asset Processor is running add secondary set of testing assets to the testing environment
5. Wait for Asset Processor to go idle
6. Verify that all assets are in the cache
"""
env = ap_setup_fixture
level_name = "C1564064_level"
new_asset = "C1564064.scriptcanvas"
@ -316,7 +358,14 @@ class TestsAssetProcessorGUI_Windows(object):
def test_WindowsPlatforms_LaunchAP_LogReportsIdle(self, asset_processor, workspace, ap_idle):
"""
Asset Processor creates a log entry when it goes idle
Test Steps:
1. Create temporary testing environment
2. Run Asset Processor batch to pre-process assets
3. Run Asset Processor GUI
4. Check if Asset Processor GUI reports that it has gone idle
"""
asset_processor.create_temp_asset_root()
# Run batch process to ensure project assets are processed
assert asset_processor.batch_process(), "AP Batch failed"
@ -331,6 +380,17 @@ class TestsAssetProcessorGUI_Windows(object):
@pytest.mark.assetpipeline
def test_APStopTimesOut_ExceptionThrown(self, ap_setup_fixture, asset_processor):
"""
Tests whether or not Asset Processor will Time Out
Test Steps:
1. Create a temporary testing environment
2. Start the Asset Processor
3. Copy in assets to the test environment
4. Try to stop the Asset Processor with a timeout of 1 second (This cannot be done manually).
5. Verify that Asset Processor times out and returns the expected error
"""
asset_processor.create_temp_asset_root()
asset_processor.start()
@ -347,9 +407,20 @@ class TestsAssetProcessorGUI_Windows(object):
@pytest.mark.assetpipeline
def test_APStopDefaultTimeout_NoException(self, asset_processor):
# If this test fails, it means other tests using the default timeout may have issues.
# In that case, either the default timeout should either be raised, or the performance
# of AP launching should be improved.
"""
Tests the default timeout of the Asset Processor
If this test fails, it means other tests using the default timeout may have issues.
In that case, either the default timeout should either be raised, or the performance
of AP launching should be improved.
Test Steps:
1. Create a temporary testing environment
2. Start the Asset Processor
3. Stop the asset Processor without sending a timeout to it
4. Verify that the asset processor times out and returns the expected error
"""
asset_processor.create_temp_asset_root()
asset_processor.start()
ap_quit_timed_out = False

@ -75,10 +75,17 @@ class TestsAssetProcessorGUI_WindowsAndMac(object):
@pytest.mark.test_case_id("C3540434")
@pytest.mark.BAT
@pytest.mark.assetpipeline
def test_WindowsAndMacPlatforms_AP_GUI_FastScanSettingCreated(self, asset_processor, fast_scan_backup):
def test_WindowsAndMacPlatforms_GUIFastScanNoSettingSet_FastScanSettingCreated(self, asset_processor, fast_scan_backup):
"""
Tests that a fast scan settings entry gets created for the AP if it does not exist
and ensures that the entry is defaulted to fast-scan enabled
Test Steps:
1. Create temporary testing environment
2. Delete existing fast scan setting if exists
3. Run Asset Processor GUI without setting FastScan setting (default:true) and without quitonidle
4. Wait and check to see if Windows Registry fast scan setting is created
5. Verify that Fast Scan setting is set to true
"""
asset_processor.create_temp_asset_root()
@ -119,6 +126,14 @@ class TestsAssetProcessorGUI_WindowsAndMac(object):
Make sure game launcher working with Asset Processor set to turbo mode
Validate that no fatal errors (crashes) are reported within a certain
time frame for the AP and the GameLauncher
Test Steps:
1. Create temporary testing environment
2. Set fast scan to true
3. Verify fast scan is set to true
4. Launch game launcher
5. Verify launcher has launched without error
6. Verify that asset processor has launched
"""
CHECK_ALIVE_SECONDS = 15
@ -166,6 +181,14 @@ class TestsAssetProcessorGUI_AllPlatforms(object):
# fmt:on
"""
Deleting slices and uicanvases while AP is running
Test Steps:
1. Create temporary testing environment with test assets
2. Launch Asset Processor and wait for it to go idle
3. Verify product assets were created in the cache
4. Delete test assets from the cache
5. Wait for Asset Processor to go idle
6. Verify product assets were regenerated in the cache
"""
env = ap_setup_fixture
@ -201,6 +224,15 @@ class TestsAssetProcessorGUI_AllPlatforms(object):
):
"""
Process slice files and uicanvas files from the additional scanfolder
Test Steps:
1. Create temporary testing environment
2. Run asset processor batch
3. Validate that product assets were generated in the cache
4. Create an additional scan folder with assets
5. Create additional scan folder params to pass to Asset Processor
6. Run Asset Processor GUI with QuitOnIdle and pass in params for the additional scan folder settings
7. Verify additional product assets from additional scan folder are present in the cache
"""
env = ap_setup_fixture
# Copy test assets to new folder in dev folder
@ -250,6 +282,12 @@ class TestsAssetProcessorGUI_AllPlatforms(object):
"""
Launch AP with invalid address in bootstrap.cfg
Assets should process regardless of the new address
Test Steps:
1. Create a temporary testing environment
2. Set an invalid ip address in Asset Processor settings file
3. Launch Asset Processor GUI
4. Verify that it processes assets and exits cleanly even though it has an invalid IP.
"""
test_ip_address = "1.1.1.1" # an IP address without Asset Processor
@ -269,6 +307,14 @@ class TestsAssetProcessorGUI_AllPlatforms(object):
def test_AllSupportedPlatforms_ModifyAssetInfo_AssetsReprocessed(self, ap_setup_fixture, asset_processor):
"""
Modifying assetinfo files triggers file reprocessing
Test Steps:
1. Create temporary testing environment with test assets
2. Run Asset Processor GUI
3. Verify that Asset Processor exited cleanly and product assets are in the cache
4. Modify the .assetinfo file by adding a newline
5. Wait for Asset Processor to go idle
6. Verify that product files were regenerated (Time Stamp compare)
"""
env = ap_setup_fixture

@ -85,6 +85,18 @@ class TestsAssetRelocator_WindowsAndMac(object):
def test_WindowsMacPlatforms_RelocatorMoveFileWithConfirm_MoveSuccess(self, request, workspace, asset_processor,
ap_setup_fixture, testId, readonly, confirm,
success):
"""
Tests whether tests with Move File Confirm are successful
Test Steps:
1. Create temporary testing environment
2. Set move location
3. Determine if confirm flag is set
4. Attempt to move the files
5. If confirm flag set:
* Validate Move was successful
* Else: Validate move was not successful
"""
env = ap_setup_fixture
copied_asset = ''
@ -141,6 +153,11 @@ class TestsAssetRelocator_WindowsAndMac(object):
User should be warned that LeaveEmptyFolders needs to be used with the move or delete command
:return: None
Test Steps:
1. Create temporary testing environment
2. Attempt to move with --LeaveEmptyFolders set
3. Verify user is given a message that command requires to be used with --move or --delete
"""
env = ap_setup_fixture
expected_message = "Command --leaveEmptyFolders must be used with command --move or --delete"
@ -162,6 +179,11 @@ class TestsAssetRelocator_WindowsAndMac(object):
Asset with UUID/AssetId reference in non-standard format is
successfully scanned and relocated to the MoveOutput folder.
This test uses a pre-corrupted .slice file.
Test Steps:
1. Create temporary testing environment with a corrupted slice
2. Attempt to move the corrupted slice
3. Verify that corrupted slice was moved successfully
"""
env = ap_setup_fixture
@ -194,6 +216,11 @@ class TestsAssetRelocator_WindowsAndMac(object):
def test_WindowsMacPlatforms_UpdateReferences_MoveCommandMessage(self, ap_setup_fixture, asset_processor):
"""
UpdateReferences without move or delete
Test Steps:
1. Create temporary testing environment
2. Attempt to move with UpdateReferences but without move or delete flags
3. Verify that message is returned to the user that additional flags are required
"""
env = ap_setup_fixture
expected_message = "Command --updateReferences must be used with command --move"
@ -215,6 +242,11 @@ class TestsAssetRelocator_WindowsAndMac(object):
"""
When running the relocator command --AllowBrokenDependencies without the move or delete flags, the user should
be warned that the flags are necessary for the functionality to be used
Test Steps:
1. Create temporary testing environment
2. Attempt to move with AllowBrokenDependencies without the move or delete flag
3. Verify that message is returned to the user that additional flags are required
"""
env = ap_setup_fixture
@ -302,10 +334,19 @@ class TestsAssetRelocator_WindowsAndMac(object):
project
):
"""
Dynamic data test for deleting a file with Asset Relocator:
C21968355 Delete a file with confirm
C21968356 Delete a file without confirm
C21968359 Delete a file that is marked as ReadOnly
C21968360 Delete a file that is not marked as ReadOnly
Test Steps:
1. Create temporary testing environment
2. Set the read-only status of the file based on the test case
3. Run asset relocator with --delete and the confirm status based on the test case
4. Assert file existence or nonexistence based on the test case
5. Validate the relocation report based on expected and unexpected messages
"""
env = ap_setup_fixture
test_file = "testFile.txt"
@ -430,6 +471,15 @@ class TestsAssetRelocator_WindowsAndMac(object):
Test the LeaveEmptyFolders flag in various configurations
:returns: None
Test Steps:
1. Create temporary testing environment
2. Build the various move/delete commands here based on test data
3. Run the move command with the various triggers based on test data
4. Verify the original assets folder still exists based on test data
5. Verify the files successfully moved to new location based on test data
6. Verify that the files were removed from original location based on test data
7. Verify the files have not been deleted or moved from original location based on test data
"""
# # Start test setup # #
env = ap_setup_fixture
@ -517,6 +567,12 @@ class TestsAssetRelocator_WindowsAndMac(object):
"""
The test will attempt to move test assets that are not tracked under P4 source control using the EnableSCM flag
Because the files are not tracked by source control, the relocation should fail
Test Steps:
1. Create temporary testing environment
2. Set ReadOnly or Not-ReadOnly for the test files based on test data
3. Generate and run the enableSCM command
4. Verify the move failed and expected messages are present
"""
# Move the test assets into the project folder
env = ap_setup_fixture
@ -1037,6 +1093,13 @@ class TestsAssetRelocator_WindowsAndMac(object):
C21968370 AllowBrokenDependencies with move and confirm
C21968371 AllowBrokenDependencies with move and without confirm
C21968375 AllowBrokenDependencies with delete
Test Steps:
1. Create temporary testing environment
2. Run Asset Processor to Process Assets
3. Build primary AP Batch parameter value and destination paths
4. Validate resulting file paths in source and output directories
5. Validate the log based on expected and unexpected messages
"""
env = ap_setup_fixture
all_test_asset_rel_paths = [
@ -1254,6 +1317,18 @@ class TestsAssetRelocator_WindowsAndMac(object):
@pytest.mark.parametrize("test", tests)
def test_WindowsAndMac_MoveMetadataFiles_PathExistenceAndMessage(self, workspace, request, ap_setup_fixture,
asset_processor, test):
"""
Tests whether moving metadata files can be moved
Test Steps:
1. Create temporary testing environment
2. Determine if using wildcards on paths or not
3. Determine if excludeMetaDataFiles is set or not
4. Build primary AP Batch parameter value and destination paths
5. Build and run the AP Batch command with parameters
6. Validate resulting file paths in source and output directories
7. Validate the log based on expected and unexpected messages
"""
env = ap_setup_fixture
def teardown():
@ -1342,7 +1417,7 @@ class TestsAssetRelocator_WindowsAndMac(object):
@dataclass
class MoveTest:
description: str # test case title directly copied from Testrail
description: str # test case title
asset_folder: str # which folder in ./assets will be used for this test
encoded_command: str # the command to execute
encoded_output_dir: str # the destination directory to validate
@ -1350,7 +1425,7 @@ class MoveTest:
name_change_map: dict = None
files_that_stay: List[str] = field(default_factory=lambda: [])
output_messages: List[str] = field(default_factory=lambda: [])
step: str = None # the step of the test from Testrail
step: str = None # the step of the test from test repository
prefix_commands: List[str] = field(default_factory=lambda: ["AssetProcessorBatch", "--zeroAnalysisMode"])
suffix_commands: List[str] = field(default_factory=lambda: ["--confirm"])
env: dict = field(init=False, default=None) # inject the ap_setup_fixture at runtime
@ -3718,7 +3793,18 @@ class TestsAssetProcessorMove_WindowsAndMac:
# -k C19462747
@pytest.mark.parametrize("test", move_a_file_tests + move_a_folder_tests)
def test_WindowsMacPlatforms_MoveCommand(self, asset_processor, ap_setup_fixture, test: MoveTest, project):
def test_WindowsMacPlatforms_MoveCommand_CommandResult(self, asset_processor, ap_setup_fixture, test: MoveTest, project):
"""
Test Steps:
1. Create temporary testing environment based on test data
2. Validate that temporary testing environment was created successfully
3. Execute the move command based upon the test data
4. Validate that files are where they're expected according to the test data
5. Validate unexpected files are not found according to the test data
6. Validate output messages according to the test data
7. Validate move status according to the test data
"""
source_folder, _ = asset_processor.prepare_test_environment(ap_setup_fixture["tests_dir"], test.asset_folder)
test.map_env(ap_setup_fixture, source_folder)

@ -75,6 +75,15 @@ class TestsMissingDependencies_WindowsAndMac(object):
def do_missing_dependency_test(self, source_product, expected_dependencies,
dsp_param,
platforms=None, max_iterations=0):
"""
Test Steps:
1. Determine what platforms to run against
2. Process assets for that platform
3. Determine the missing dependency params to set
4. Set the max iteration param
5. Run missing dependency scanner against target platforms and search params based on test data
6. Validate missing dependencies against test data
"""
platforms = platforms or ASSET_PROCESSOR_PLATFORM_MAP[self._workspace.asset_processor_platform]
if not isinstance(platforms, list):
@ -104,7 +113,14 @@ class TestsMissingDependencies_WindowsAndMac(object):
@pytest.mark.assetpipeline
@pytest.mark.test_case_id("C17226567")
def test_WindowsAndMac_ValidUUIDNotDependency_ReportsMissingDependency(self):
"""Tests that a valid UUID referenced in a file will report any missing dependencies"""
"""
Tests that a valid UUID referenced in a file will report any missing dependencies
Test Steps:
1. Set the expected product
2. Set the expected missing dependencies
3. Execute test
"""
# Relative path to the txt file with missing dependencies
expected_product = f"testassets\\validuuidsnotdependency.txt"
@ -141,7 +157,14 @@ class TestsMissingDependencies_WindowsAndMac(object):
@pytest.mark.assetpipeline
@pytest.mark.test_case_id("C17226567")
def test_WindowsAndMac_InvalidUUIDsNotDependencies_NoReportedMessage(self):
"""Tests that invalid UUIDs do not count as missing dependencies"""
"""
Tests that invalid UUIDs do not count as missing dependencies
Test Steps:
1. Set the expected product
2. Set the expected missing dependencies
3. Execute test
"""
# Relative path to the txt file with invalid UUIDs
expected_product = f"testassets\\invaliduuidnoreport.txt"
expected_dependencies = [] # No expected missing dependencies
@ -153,7 +176,14 @@ class TestsMissingDependencies_WindowsAndMac(object):
@pytest.mark.assetpipeline
@pytest.mark.test_case_id("C17226567")
def test_WindowsAndMac_ValidAssetIdsNotDependencies_ReportsMissingDependency(self):
"""Tests that valid asset IDs but not dependencies, show missing dependencies"""
"""
Tests that valid asset IDs but not dependencies, show missing dependencies
Test Steps:
1. Set the expected product
2. Set the expected missing dependencies
3. Execute test
"""
# Relative path to the txt file with valid asset ids but not dependencies
expected_product = f"testassets\\validassetidnotdependency.txt"
@ -173,7 +203,14 @@ class TestsMissingDependencies_WindowsAndMac(object):
@pytest.mark.assetpipeline
@pytest.mark.test_case_id("C17226567")
def test_WindowsAndMac_InvalidAssetsIDNotDependencies_NoReportedMessage(self):
"""Tests that invalid asset IDs do not count as missing dependencies"""
"""
Tests that invalid asset IDs do not count as missing dependencies
Test Steps:
1. Set the expected product
2. Set the expected missing dependencies
3. Execute test
"""
# Relative path to the txt file with invalid asset IDs
expected_product = f"testassets\\invalidassetidnoreport.txt"
@ -188,7 +225,14 @@ class TestsMissingDependencies_WindowsAndMac(object):
# fmt:off
def test_WindowsAndMac_ValidSourcePathsNotDependencies_ReportsMissingDependencies(self):
# fmt:on
"""Tests that valid source paths can translate to missing dependencies"""
"""
Tests that valid source paths can translate to missing dependencies
Test Steps:
1. Set the expected product
2. Set the expected missing dependencies
3. Execute test
"""
# Relative path to the txt file with missing dependencies as source paths
expected_product = f"testassets\\relativesourcepathsnotdependencies.txt"
@ -212,7 +256,14 @@ class TestsMissingDependencies_WindowsAndMac(object):
@pytest.mark.assetpipeline
@pytest.mark.test_case_id("C17226567")
def test_WindowsAndMac_InvalidARelativePathsNotDependencies_NoReportedMessage(self):
"""Tests that invalid relative paths do not resolve to missing dependencies"""
"""
Tests that invalid relative paths do not resolve to missing dependencies
Test Steps:
1. Set the expected product
2. Set the expected missing dependencies
3. Execute test
"""
# Relative path to the txt file with invalid relative paths
expected_product = f"testassets\\invalidrelativepathsnoreport.txt"
@ -227,7 +278,14 @@ class TestsMissingDependencies_WindowsAndMac(object):
# fmt:off
def test_WindowsAndMac_ValidProductPathsNotDependencies_ReportsMissingDependencies(self):
# fmt:on
"""Tests that valid product paths can resolve to missing dependencies"""
"""
Tests that valid product paths can resolve to missing dependencies
Test Steps:
1. Set the expected product
2. Set the expected missing dependencies
3. Execute test
"""
self._asset_processor.add_source_folder_assets(f"Gems\\LyShineExamples\\Assets\\UI\\Fonts\\LyShineExamples")
self._asset_processor.add_scan_folder(f"Gems\\LyShineExamples\\Assets")
@ -260,7 +318,14 @@ class TestsMissingDependencies_WindowsAndMac(object):
@pytest.mark.assetpipeline
@pytest.mark.test_case_id("C17226567")
def test_WindowsAndMac_WildcardScan_FindsAllExpectedFiles(self):
"""Tests that the wildcard scanning will pick up multiple files"""
"""
Tests that the wildcard scanning will pick up multiple files
Test Steps:
1. Set the expected product
2. Set the expected missing dependencies
3. Execute test
"""
helper = self._missing_dep_helper
@ -291,6 +356,11 @@ class TestsMissingDependencies_WindowsAndMac(object):
For these references that are valid, all but one have available, matching dependencies. This test is
primarily meant to verify that the missing dependency reporter checks the product dependency table before
emitting missing dependencies.
Test Steps:
1. Set the expected product
2. Set the expected missing dependencies
3. Execute test
"""
# Relative path to target test file
expected_product = f"testassets\\reportonemissingdependency.txt"
@ -305,7 +375,14 @@ class TestsMissingDependencies_WindowsAndMac(object):
@pytest.mark.assetpipeline
@pytest.mark.test_case_id("C17226567")
def test_WindowsAndMac_ReferencesSelfPath_NoReportedMessage(self):
"""Tests that a file that references itself via relative path does not report itself as a missing dependency"""
"""
Tests that a file that references itself via relative path does not report itself as a missing dependency
Test Steps:
1. Set the expected product
2. Set the expected missing dependencies
3. Execute test
"""
# Relative path to file that references itself via relative path
expected_product = f"testassets\\selfreferencepath.txt"
expected_dependencies = []
@ -317,7 +394,14 @@ class TestsMissingDependencies_WindowsAndMac(object):
@pytest.mark.assetpipeline
@pytest.mark.test_case_id("C17226567")
def test_WindowsAndMac_ReferencesSelfUUID_NoReportedMessage(self):
"""Tests that a file that references itself via its UUID does not report itself as a missing dependency"""
"""
Tests that a file that references itself via its UUID does not report itself as a missing dependency
Test Steps:
1. Set the expected product
2. Set the expected missing dependencies
3. Execute test
"""
# Relative path to file that references itself via its UUID
expected_product = f"testassets\\selfreferenceuuid.txt"
@ -330,7 +414,14 @@ class TestsMissingDependencies_WindowsAndMac(object):
@pytest.mark.assetpipeline
@pytest.mark.test_case_id("C17226567")
def test_WindowsAndMac_ReferencesSelfAssetID_NoReportedMessage(self):
"""Tests that a file that references itself via its Asset ID does not report itself as a missing dependency"""
"""
Tests that a file that references itself via its Asset ID does not report itself as a missing dependency
Test Steps:
1. Set the expected product
2. Set the expected missing dependencies
3. Execute test
"""
# Relative path to file that references itself via its Asset ID
expected_product = f"testassets\\selfreferenceassetid.txt"
@ -347,6 +438,11 @@ class TestsMissingDependencies_WindowsAndMac(object):
Tests that the scan limit fails to find a missing dependency that is out of reach.
The max iteration count is set to just under where a valid missing dependency is on a line in the file,
so this will not report any missing dependencies.
Test Steps:
1. Set the expected product
2. Set the expected missing dependencies
3. Execute test
"""
# Relative path to file that has a missing dependency at 31 iterations deep
@ -364,7 +460,13 @@ class TestsMissingDependencies_WindowsAndMac(object):
Tests that the scan limit succeeds in finding a missing dependency that is barely in reach.
In the previous test, the scanner was set to stop recursion just before a missing dependency was found.
This test runs with the recursion limit set deep enough to actually find the missing dependency.
Test Steps:
1. Set the expected product
2. Set the expected missing dependencies
3. Execute test
"""
# Relative path to file that has a missing dependency at 31 iterations deep
expected_product = f"testassets\\maxiteration31deep.txt"
@ -383,7 +485,14 @@ class TestsMissingDependencies_WindowsAndMac(object):
# fmt:off
def test_WindowsAndMac_PotentialMatchesLongerThanUUIDString_OnlyReportsCorrectLengthUUIDs(self):
# fmt:on
"""Tests that dependency references that are longer than expected are ignored"""
"""
Tests that dependency references that are longer than expected are ignored
Test Steps:
1. Set the expected product
2. Set the expected missing dependencies
3. Execute test
"""
# Relative path to text file with varying length UUID references
expected_product = f"testassets\\onlymatchescorrectlengthuuids.txt"
@ -408,7 +517,14 @@ class TestsMissingDependencies_WindowsAndMac(object):
def test_WindowsAndMac_MissingDependencyScanner_GradImageSuccess(
self, ap_setup_fixture
):
"""Tests the Missing Dependency Scanner can scan gradimage files"""
"""
Tests the Missing Dependency Scanner can scan gradimage files
Test Steps:
1. Create temporary testing environment
2. Run the move dependency scanner against the gradimage
2. Validate that the expected product files and and expected depdencies match
"""
env = ap_setup_fixture
helper = self._missing_dep_helper

@ -51,6 +51,11 @@ class TestAuxiliaryContent:
def test_CreateAuxiliaryContent_DontSkipLevelPaks(self, workspace, level):
"""
This test ensure that Auxiliary Content contain level.pak files
Test Steps:
1. Run auxiliary content against project under test
2. Validate auxiliary content exists
3. Verifies that level.pak exists
"""
path_to_dev = workspace.paths.engine_root()
@ -70,6 +75,11 @@ class TestAuxiliaryContent:
def test_CreateAuxiliaryContent_SkipLevelPaks(self, workspace, level):
"""
This test ensure that Auxiliary Content contain no level.pak file
Test Steps:
1. Run auxiliary content against project under test with skiplevelPaks flag
2. Validate auxiliary content exists
3. Validate level.pak was added to auxiliary content
"""
path_to_dev = workspace.paths.engine_root()

@ -533,6 +533,14 @@ class TestsFBX_AllPlatforms(object):
def test_FBXBlackboxTest_SourceFiles_Processed_ResultInExpectedProducts(self, workspace,
ap_setup_fixture, asset_processor, project,
blackbox_param):
"""
Please see run_fbx_test(...) for details
Test Steps:
1. Determine if blackbox is set to none
2. Run FBX Test
"""
if blackbox_param == None:
return
self.run_fbx_test(workspace, ap_setup_fixture,
@ -544,6 +552,15 @@ class TestsFBX_AllPlatforms(object):
workspace, ap_setup_fixture,
asset_processor, project,
blackbox_param):
"""
Please see run_fbx_test(...) for details
Test Steps:
1. Determine if blackbox is set to none
2. Run FBX Test
2. Re-run FBX test and validate the information in override assets
"""
if blackbox_param == None:
return
self.run_fbx_test(workspace, ap_setup_fixture,
@ -567,6 +584,19 @@ class TestsFBX_AllPlatforms(object):
def run_fbx_test(self, workspace, ap_setup_fixture, asset_processor,
project, blackbox_params: BlackboxAssetTest, overrideAsset = False):
"""
These tests work by having the test case ingest the test data and determine the run pattern.
Tests will process scene settings files and will additionally do a verification against a provided debug file
Additionally, if an override is passed, the output is checked against the override.
Test Steps:
1. Create temporary test environment
2. Process Assets
3. Determine what assets to validate based upon test data
4. Validate assets were created in cache
5. If debug file provided, verify scene files were generated correctly
6. Verify that each given source asset resulted in the expected jobs and products
"""
test_assets_folder = blackbox_params.override_asset_folder if overrideAsset else blackbox_params.asset_folder
logger.info(f"{blackbox_params.test_name}: Processing assets in folder '"

@ -26,6 +26,18 @@ def soundbank_metadata_generator_setup_fixture(workspace):
def success_case_test(test_folder, expected_dependencies_dict, bank_info, expected_result_code=0):
"""
Test Steps:
1. Make sure the return code is what was expected, and that the expected number of banks were returned.
2. Validate bank is in the expected dependencies dictionary.
3. Validate the path to output the metadata file to was assembled correctly.
4. Validate metadata object for this bank is set, and that it has an object assigned to its dependencies field
and its includedEvents field
5. Validate metadata object has the correct number of dependencies, and validated that every expected dependency
exists in the dependencies list of the metadata object.
6. Validate metadata object has the correct number of events, and validate that every expected event exists in the
events of the metadata object.
"""
expected_bank_count = len(expected_dependencies_dict)
banks, result_code = bank_info.generate_metadata(
@ -80,8 +92,17 @@ class TestSoundBankMetadataGenerator:
def test_NoMetadataTooFewBanks_ReturnCodeIsError(self, workspace, soundbank_metadata_generator_setup_fixture):
# Trying to generate metadata for banks in a folder with one or fewer banks and no metadata is not possible
# and should fail.
"""
Trying to generate metadata for banks in a folder with one or fewer banks and no metadata is not possible
and should fail.
Test Steps:
1. Setup testing environment with only 1 bank file
2. Get Sound Bank Info
3. Attempt to generate sound bank metadata
4. Verify that proper error code is returned
"""
#
test_assets_folder = os.path.join(soundbank_metadata_generator_setup_fixture['tests_dir'], 'assets',
'test_NoMetadataTooFewBanks_ReturnCodeIsError')
if not os.path.isdir(test_assets_folder):
@ -97,15 +118,30 @@ class TestSoundBankMetadataGenerator:
assert error_code is 2, 'Metadata was generated when there were fewer than two banks in the target directory.'
def test_NoMetadataNoContentBank_NoMetadataGenerated(self, workspace, soundbank_metadata_generator_setup_fixture):
"""
Test Steps:
1. Setup testing environment
2. No expected dependencies
3. Call success case test
"""
test_assets_folder = os.path.join(soundbank_metadata_generator_setup_fixture['tests_dir'], 'assets',
'test_NoMetadataNoContentBank_NoMetadataGenerated')
expected_dependencies = dict()
success_case_test(test_assets_folder, expected_dependencies, get_bank_info(workspace))
def test_NoMetadataOneContentBank_NoStreamedFiles_OneDependency(self, workspace, soundbank_metadata_generator_setup_fixture):
# When no Wwise metadata is present, and there is only one content bank in the target directory with no wem
# files, then only the content bank should have metadata associated with it. The generated metadata should
# only describe a dependency on the init bank.
"""
When no Wwise metadata is present, and there is only one content bank in the target directory with no wem
files, then only the content bank should have metadata associated with it. The generated metadata should
only describe a dependency on the init bank.
Test Steps:
1. Setup testing environment
2. Get current bank info
3. Build expected dependencies
4. Call success case test
"""
test_assets_folder = os.path.join(soundbank_metadata_generator_setup_fixture['tests_dir'], 'assets',
'test_NoMetadataOneContentBank_NoStreamedFiles_OneDependency')
@ -116,9 +152,18 @@ class TestSoundBankMetadataGenerator:
def test_NoMetadataOneContentBank_StreamedFiles_MultipleDependencies(self, workspace,
soundbank_metadata_generator_setup_fixture):
# When no Wwise metadata is present, and there is only one content bank in the target directory with wem files
# present, then only the content bank should have metadata associated with it. The generated metadata should
# describe a dependency on the init bank and all wem files in the folder.
"""
When no Wwise metadata is present, and there is only one content bank in the target directory with wem files
present, then only the content bank should have metadata associated with it. The generated metadata should
describe a dependency on the init bank and all wem files in the folder.
Test Steps:
1. Setup testing environment
2. Get current bank info
3. Build expected dependencies
4. Call success case test
"""
test_assets_folder = os.path.join(soundbank_metadata_generator_setup_fixture['tests_dir'], 'assets',
'test_NoMetadataOneContentBank_StreamedFiles_MultipleDependencies')
@ -136,10 +181,19 @@ class TestSoundBankMetadataGenerator:
success_case_test(test_assets_folder, expected_dependencies, get_bank_info(workspace))
def test_NoMetadataMultipleBanks_OneDependency_ReturnCodeIsWarning(self, workspace, soundbank_metadata_generator_setup_fixture):
# When no Wwise metadata is present, and there are multiple content banks in the target directory with wem files
# present, there is no way to tell which bank requires which wem files. A warning should be emitted,
# stating that the full dependency graph could not be created, and only dependencies on the init bank are
# described in the generated metadata files.
"""
When no Wwise metadata is present, and there are multiple content banks in the target directory with wem files
present, there is no way to tell which bank requires which wem files. A warning should be emitted,
stating that the full dependency graph could not be created, and only dependencies on the init bank are
described in the generated metadata files.
Test Steps:
1. Setup testing environment
2. Get current bank info
3. Build expected dependencies
4. Call success case test
"""
test_assets_folder = os.path.join(soundbank_metadata_generator_setup_fixture['tests_dir'], 'assets',
'test_NoMetadataMultipleBanks_OneDependency_ReturnCodeIsWarning')
bank_info = get_bank_info(workspace)
@ -150,8 +204,17 @@ class TestSoundBankMetadataGenerator:
success_case_test(test_assets_folder, expected_dependencies, get_bank_info(workspace), expected_result_code=1)
def test_OneContentBank_NoStreamedFiles_OneDependency(self, workspace, soundbank_metadata_generator_setup_fixture):
# Wwise metadata describes one content bank that contains all media needed by its events. Generated metadata
# describes a dependency only on the init bank.
"""
Wwise metadata describes one content bank that contains all media needed by its events. Generated metadata
describes a dependency only on the init bank.
Test Steps:
1. Setup testing environment
2. Get current bank info
3. Build expected dependencies
4. Call success case test
"""
test_assets_folder = os.path.join(soundbank_metadata_generator_setup_fixture['tests_dir'], 'assets',
'test_OneContentBank_NoStreamedFiles_OneDependency')
@ -165,8 +228,17 @@ class TestSoundBankMetadataGenerator:
success_case_test(test_assets_folder, expected_dependencies, get_bank_info(workspace))
def test_OneContentBank_StreamedFiles_MultipleDependencies(self, workspace, soundbank_metadata_generator_setup_fixture):
# Wwise metadata describes one content bank that references streamed media files needed by its events. Generated
# metadata describes dependencies on the init bank and wems named by the IDs of referenced streamed media.
"""
Wwise metadata describes one content bank that references streamed media files needed by its events. Generated
metadata describes dependencies on the init bank and wems named by the IDs of referenced streamed media.
Test Steps:
1. Setup testing environment
2. Get current bank info
3. Build expected dependencies
4. Call success case test
"""
test_assets_folder = os.path.join(soundbank_metadata_generator_setup_fixture['tests_dir'], 'assets',
'test_OneContentBank_StreamedFiles_MultipleDependencies')
@ -187,8 +259,17 @@ class TestSoundBankMetadataGenerator:
success_case_test(test_assets_folder, expected_dependencies, get_bank_info(workspace))
def test_MultipleContentBanks_NoStreamedFiles_OneDependency(self, workspace, soundbank_metadata_generator_setup_fixture):
# Wwise metadata describes multiple content banks. Each bank contains all media needed by its events. Generated
# metadata describes each bank having a dependency only on the init bank.
"""
Wwise metadata describes multiple content banks. Each bank contains all media needed by its events. Generated
metadata describes each bank having a dependency only on the init bank.
Test Steps:
1. Setup testing environment
2. Get current bank info
3. Build expected dependencies
4. Call success case test
"""
test_assets_folder = os.path.join(soundbank_metadata_generator_setup_fixture['tests_dir'], 'assets',
'test_MultipleContentBanks_NoStreamedFiles_OneDependency')
@ -206,8 +287,17 @@ class TestSoundBankMetadataGenerator:
success_case_test(test_assets_folder, expected_dependencies, get_bank_info(workspace))
def test_MultipleContentBanks_Bank1StreamedFiles(self, workspace, soundbank_metadata_generator_setup_fixture):
# Wwise metadata describes multiple content banks. Bank 1 references streamed media files needed by its events,
# while bank 2 contains all media need by its events.
"""
Wwise metadata describes multiple content banks. Bank 1 references streamed media files needed by its events,
while bank 2 contains all media need by its events.
Test Steps:
1. Setup testing environment
2. Get current bank info
3. Build expected dependencies
4. Call success case test
"""
test_assets_folder = os.path.join(soundbank_metadata_generator_setup_fixture['tests_dir'], 'assets',
'test_MultipleContentBanks_Bank1StreamedFiles')
@ -228,9 +318,18 @@ class TestSoundBankMetadataGenerator:
success_case_test(test_assets_folder, expected_dependencies, get_bank_info(workspace))
def test_MultipleContentBanks_SplitBanks_OnlyBankDependenices(self, workspace, soundbank_metadata_generator_setup_fixture):
# Wwise metadata describes multiple content banks. Bank 3 events require media that is contained in bank 4.
# Generated metadata describes each bank having a dependency on the init bank, while bank 3 has an additional
# dependency on bank 4.
"""
Wwise metadata describes multiple content banks. Bank 3 events require media that is contained in bank 4.
Generated metadata describes each bank having a dependency on the init bank, while bank 3 has an additional
dependency on bank 4.
Test Steps:
1. Setup testing environment
2. Get current bank info
3. Build expected dependencies
4. Call success case test
"""
test_assets_folder = os.path.join(soundbank_metadata_generator_setup_fixture['tests_dir'], 'assets',
'test_MultipleContentBanks_SplitBanks_OnlyBankDependenices')
@ -248,9 +347,18 @@ class TestSoundBankMetadataGenerator:
success_case_test(test_assets_folder, expected_dependencies, get_bank_info(workspace))
def test_MultipleContentBanks_ReferencedEvent_MediaEmbeddedInBank(self, workspace, soundbank_metadata_generator_setup_fixture):
# Wwise metadata describes multiple content banks. Bank 1 contains all media required by its events, while bank
# 5 contains a reference to an event in bank 1, but no media for that event. Generated metadata describes both
# banks having a dependency on the init bank, while bank 5 has an additional dependency on bank 1.
"""
Wwise metadata describes multiple content banks. Bank 1 contains all media required by its events, while bank
5 contains a reference to an event in bank 1, but no media for that event. Generated metadata describes both
banks having a dependency on the init bank, while bank 5 has an additional dependency on bank 1.
Test Steps:
1. Setup testing environment
2. Get current bank info
3. Build expected dependencies
4. Call success case test
"""
test_assets_folder = os.path.join(soundbank_metadata_generator_setup_fixture['tests_dir'], 'assets',
'test_MultipleContentBanks_ReferencedEvent_MediaEmbeddedInBank')
@ -271,10 +379,19 @@ class TestSoundBankMetadataGenerator:
success_case_test(test_assets_folder, expected_dependencies, get_bank_info(workspace))
def test_MultipleContentBanks_ReferencedEvent_MediaStreamed(self, workspace, soundbank_metadata_generator_setup_fixture):
# Wwise metadata describes multiple content banks. Bank 1 references streamed media files needed by its events,
# while bank 5 contains a reference to an event in bank 1. This causes bank 5 to also describe a reference to
# the streamed media file referenced by the event from bank 1. Generated metadata describes both banks having
# dependencies on the init bank, as well as the wem named by the ID of referenced streamed media.
"""
Wwise metadata describes multiple content banks. Bank 1 references streamed media files needed by its events,
while bank 5 contains a reference to an event in bank 1. This causes bank 5 to also describe a reference to
the streamed media file referenced by the event from bank 1. Generated metadata describes both banks having
dependencies on the init bank, as well as the wem named by the ID of referenced streamed media.
Test Steps:
1. Setup testing environment
2. Get current bank info
3. Build expected dependencies
4. Call success case test
"""
test_assets_folder = os.path.join(soundbank_metadata_generator_setup_fixture['tests_dir'], 'assets',
'test_MultipleContentBanks_ReferencedEvent_MediaStreamed')
@ -298,11 +415,20 @@ class TestSoundBankMetadataGenerator:
success_case_test(test_assets_folder, expected_dependencies, get_bank_info(workspace))
def test_MultipleContentBanks_ReferencedEvent_MixedSources(self, workspace, soundbank_metadata_generator_setup_fixture):
# Wwise metadata describes multiple content banks. Bank 1 references a streamed media files needed by one of its
# events, and contains all media needed for its other events, while bank 5 contains a reference to two events
# in bank 1: one that requires streamed media, and one that requires media embedded in bank 1. Generated
# metadata describes both banks having dependencies on the init bank and the wem named by the ID of referenced
# streamed media, while bank 5 has an additional dependency on bank 1.
"""
Wwise metadata describes multiple content banks. Bank 1 references a streamed media files needed by one of its
events, and contains all media needed for its other events, while bank 5 contains a reference to two events
in bank 1: one that requires streamed media, and one that requires media embedded in bank 1. Generated
metadata describes both banks having dependencies on the init bank and the wem named by the ID of referenced
streamed media, while bank 5 has an additional dependency on bank 1.
Test Steps:
1. Setup testing environment
2. Get current bank info
3. Build expected dependencies
4. Call success case test
"""
test_assets_folder = os.path.join(soundbank_metadata_generator_setup_fixture['tests_dir'], 'assets',
'test_MultipleContentBanks_ReferencedEvent_MixedSources')
@ -332,8 +458,17 @@ class TestSoundBankMetadataGenerator:
success_case_test(test_assets_folder, expected_dependencies, get_bank_info(workspace))
def test_MultipleContentBanks_VaryingDependencies_MixedSources(self, workspace, soundbank_metadata_generator_setup_fixture):
# Wwise metadata describes multiple content banks that have varying dependencies on each other, and dependencies
# on streamed media files.
"""
Wwise metadata describes multiple content banks that have varying dependencies on each other, and dependencies
on streamed media files.
Test Steps:
1. Setup testing environment
2. Get current bank info
3. Build expected dependencies
4. Call success case test
"""
test_assets_folder = os.path.join(soundbank_metadata_generator_setup_fixture['tests_dir'], 'assets',
'test_MultipleContentBanks_VaryingDependencies_MixedSources')

@ -0,0 +1,131 @@
"""
All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
its licensors.
For complete copyright and license terms please see the LICENSE at the root of this
distribution (the "License"). All use of this software is governed by the License,
or, if provided, by the license below or the license accompanying this file. Do not
remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
"""
# fmt: off
class Tests():
new_event_created = ("Successfully created a new event", "Failed to create a new event")
child_event_created = ("Successfully created Child Event", "Failed to create Child Event")
file_saved = ("Successfully saved event asset", "Failed to save event asset")
parameter_created = ("Successfully added parameter", "Failed to add parameter")
parameter_removed = ("Successfully removed parameter", "Failed to remove parameter")
# fmt: on
def ScriptEvent_AddRemoveParameter_ActionsSuccessful():
"""
Summary:
Parameter can be removed from a Script Event method
Expected Behavior:
Upon saving the updated .scriptevents asset the removed paramenter should no longer be present on the Script Event
Test Steps:
1) Open Asset Editor
2) Get Asset Editor Qt object
3) Create new Script Event Asset
4) Add Parameter to Event
5) Remove Parameter from Event
Note:
- This test file must be called from the Open 3D Engine Editor command terminal
- Any passed and failed tests are written to the Editor.log file.
Parsing the file or running a log_monitor are required to observe the test results.
:return: None
"""
import os
from PySide2 import QtWidgets
from editor_python_test_tools.utils import Report
from editor_python_test_tools.utils import TestHelper as helper
import editor_python_test_tools.pyside_utils as pyside_utils
import azlmbr.bus as bus
import azlmbr.editor as editor
import azlmbr.legacy.general as general
GENERAL_WAIT = 1.0 # seconds
FILE_PATH = os.path.join("AutomatedTesting", "ScriptCanvas", "test_file.scriptevent")
QtObject = object
def create_script_event(asset_editor: QtObject, file_path: str) -> None:
action = pyside_utils.find_child_by_pattern(menu_bar, {"type": QtWidgets.QAction, "text": "Script Events"})
action.trigger()
result = helper.wait_for_condition(
lambda: container.findChild(QtWidgets.QFrame, "Events") is not None, 3 * GENERAL_WAIT
)
Report.result(Tests.new_event_created, result)
# Add new child event
add_event = container.findChild(QtWidgets.QFrame, "Events").findChild(QtWidgets.QToolButton, "")
add_event.click()
result = helper.wait_for_condition(
lambda: asset_editor.findChild(QtWidgets.QFrame, "EventName") is not None, GENERAL_WAIT
)
Report.result(Tests.child_event_created, result)
# Save the Script Event file
editor.AssetEditorWidgetRequestsBus(bus.Broadcast, "SaveAssetAs", file_path)
# Verify if file is created
result = helper.wait_for_condition(lambda: os.path.exists(file_path), 3 * GENERAL_WAIT)
Report.result(Tests.file_saved, result)
def create_parameter(file_path: str) -> None:
add_param = container.findChild(QtWidgets.QFrame, "Parameters").findChild(QtWidgets.QToolButton, "")
add_param.click()
result = helper.wait_for_condition(
lambda: asset_editor_widget.findChild(QtWidgets.QFrame, "[0]") is not None, GENERAL_WAIT
)
Report.result(Tests.parameter_created, result)
editor.AssetEditorWidgetRequestsBus(bus.Broadcast, "SaveAssetAs", file_path)
def remove_parameter(file_path: str) -> None:
remove_param = container.findChild(QtWidgets.QFrame, "[0]").findChild(QtWidgets.QToolButton, "")
remove_param.click()
result = helper.wait_for_condition(
lambda: asset_editor_widget.findChild(QtWidgets.QFrame, "[0]") is None, GENERAL_WAIT
)
Report.result(Tests.parameter_removed, result)
editor.AssetEditorWidgetRequestsBus(bus.Broadcast, "SaveAssetAs", file_path)
# 1) Open Asset Editor
general.idle_enable(True)
# Initially close the Asset Editor and then reopen to ensure we don't have any existing assets open
general.close_pane("Asset Editor")
general.open_pane("Asset Editor")
helper.wait_for_condition(lambda: general.is_pane_visible("Asset Editor"), 5.0)
# 2) Get Asset Editor Qt object
editor_window = pyside_utils.get_editor_main_window()
asset_editor_widget = editor_window.findChild(QtWidgets.QDockWidget, "Asset Editor").findChild(
QtWidgets.QWidget, "AssetEditorWindowClass"
)
container = asset_editor_widget.findChild(QtWidgets.QWidget, "ContainerForRows")
menu_bar = asset_editor_widget.findChild(QtWidgets.QMenuBar)
# 3) Create new Script Event Asset
create_script_event(asset_editor_widget, FILE_PATH)
# 4) Add Parameter to Event
create_parameter(FILE_PATH)
# 5) Remove Parameter from Event
remove_parameter(FILE_PATH)
if __name__ == "__main__":
import ImportPathHelper as imports
imports.init()
from editor_python_test_tools.utils import Report
Report.start_test(ScriptEvent_AddRemoveParameter_ActionsSuccessful)

@ -186,6 +186,18 @@ class TestAutomation(TestAutomationBase):
from . import Node_HappyPath_DuplicateNode as test_module
self._run_test(request, workspace, editor, test_module)
def test_ScriptEvent_AddRemoveParameter_ActionsSuccessful(self, request, workspace, editor, launcher_platform):
def teardown():
file_system.delete(
[os.path.join(workspace.paths.project(), "ScriptCanvas", "test_file.scriptevent")], True, True
)
request.addfinalizer(teardown)
file_system.delete(
[os.path.join(workspace.paths.project(), "ScriptCanvas", "test_file.scriptevent")], True, True
)
from . import ScriptEvent_AddRemoveParameter_ActionsSuccessful as test_module
self._run_test(request, workspace, editor, test_module)
# NOTE: We had to use hydra_test_utils.py, as TestAutomationBase run_test method
# fails because of pyside_utils import
@pytest.mark.SUITE_periodic

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:804193a2afd68cd1e6bec8155ea11400566f2941fbd6eb0c324839ebcd10192d
size 8492
oid sha256:302d6172156e8ed665e44e206d81f54f1b0f1008d73327300ea92f8c1159780b
size 11820

@ -3,7 +3,7 @@
{
"AWSCore":
{
"ProfileName": "default",
"ProfileName": "AWSAutomationTest",
"ResourceMappingConfigFileName": "aws_resource_mappings.json"
}
}

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:40949893ed7009eeaa90b7ce6057cb6be9dfaf7b162e3c26ba9dadf985939d7d
size 2038
oid sha256:b9cd9d6f67440c193a85969ec5c082c6343e6d1fff3b6f209a0a6931eb22dd47
size 2949

@ -4,17 +4,152 @@
<Class name="AZStd::vector" field="Properties" type="{A8E59F8C-2F9A-525A-B549-A9E197EB9632}">
<Class name="Physics::MaterialFromAssetConfiguration" field="element" version="1" type="{FBD76628-DE57-435E-BE00-6FFAE64DDF1D}">
<Class name="Physics::MaterialConfiguration" field="Configuration" version="3" type="{8807CAA1-AD08-4238-8FDB-2154ADD084A1}">
<Class name="AZStd::string" field="SurfaceType" value="Debug" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
<Class name="float" field="DynamicFriction" value="0.0000000" type="{EA2C3E90-AFBE-44D4-A90D-FAAF79BAF93D}"/>
<Class name="float" field="StaticFriction" value="0.0000000" type="{EA2C3E90-AFBE-44D4-A90D-FAAF79BAF93D}"/>
<Class name="float" field="Restitution" value="0.0000000" type="{EA2C3E90-AFBE-44D4-A90D-FAAF79BAF93D}"/>
<Class name="AZStd::string" field="SurfaceType" value="Character" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
<Class name="float" field="DynamicFriction" value="0.7000000" type="{EA2C3E90-AFBE-44D4-A90D-FAAF79BAF93D}"/>
<Class name="float" field="StaticFriction" value="0.8000000" type="{EA2C3E90-AFBE-44D4-A90D-FAAF79BAF93D}"/>
<Class name="float" field="Restitution" value="0.3000000" type="{EA2C3E90-AFBE-44D4-A90D-FAAF79BAF93D}"/>
<Class name="unsigned char" field="FrictionCombine" value="2" type="{72B9409A-7D1A-4831-9CFE-FCB3FADD3426}"/>
<Class name="unsigned char" field="RestitutionCombine" value="3" type="{72B9409A-7D1A-4831-9CFE-FCB3FADD3426}"/>
<Class name="float" field="Density" value="985.0000000" type="{EA2C3E90-AFBE-44D4-A90D-FAAF79BAF93D}"/>
<Class name="Color" field="DebugColor" value="0.9183642 0.6973526 0.4447700 1.0000000" type="{7894072A-9050-4F0F-901B-34B1A0D29417}"/>
</Class>
<Class name="Physics::MaterialId" field="UID" version="1" type="{744CCE6C-9F69-4E2F-B950-DAB8514F870B}">
<Class name="AZ::Uuid" field="MaterialId" value="{FDECD8B6-5BAF-42CB-AEFE-C66E1E1CF557}" type="{E152C105-A133-4D03-BBF8-3D4B2FBA3E2A}"/>
</Class>
</Class>
<Class name="Physics::MaterialFromAssetConfiguration" field="element" version="1" type="{FBD76628-DE57-435E-BE00-6FFAE64DDF1D}">
<Class name="Physics::MaterialConfiguration" field="Configuration" version="3" type="{8807CAA1-AD08-4238-8FDB-2154ADD084A1}">
<Class name="AZStd::string" field="SurfaceType" value="Concrete" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
<Class name="float" field="DynamicFriction" value="0.8000000" type="{EA2C3E90-AFBE-44D4-A90D-FAAF79BAF93D}"/>
<Class name="float" field="StaticFriction" value="0.9000000" type="{EA2C3E90-AFBE-44D4-A90D-FAAF79BAF93D}"/>
<Class name="float" field="Restitution" value="0.3800000" type="{EA2C3E90-AFBE-44D4-A90D-FAAF79BAF93D}"/>
<Class name="unsigned char" field="FrictionCombine" value="3" type="{72B9409A-7D1A-4831-9CFE-FCB3FADD3426}"/>
<Class name="unsigned char" field="RestitutionCombine" value="0" type="{72B9409A-7D1A-4831-9CFE-FCB3FADD3426}"/>
<Class name="float" field="Density" value="2400.0000000" type="{EA2C3E90-AFBE-44D4-A90D-FAAF79BAF93D}"/>
<Class name="Color" field="DebugColor" value="0.5918365 0.4927596 0.3795224 1.0000000" type="{7894072A-9050-4F0F-901B-34B1A0D29417}"/>
</Class>
<Class name="Physics::MaterialId" field="UID" version="1" type="{744CCE6C-9F69-4E2F-B950-DAB8514F870B}">
<Class name="AZ::Uuid" field="MaterialId" value="{A9CACCFF-E0D2-4149-8891-E92319229B2D}" type="{E152C105-A133-4D03-BBF8-3D4B2FBA3E2A}"/>
</Class>
</Class>
<Class name="Physics::MaterialFromAssetConfiguration" field="element" version="1" type="{FBD76628-DE57-435E-BE00-6FFAE64DDF1D}">
<Class name="Physics::MaterialConfiguration" field="Configuration" version="3" type="{8807CAA1-AD08-4238-8FDB-2154ADD084A1}">
<Class name="AZStd::string" field="SurfaceType" value="Glass" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
<Class name="float" field="DynamicFriction" value="0.4000000" type="{EA2C3E90-AFBE-44D4-A90D-FAAF79BAF93D}"/>
<Class name="float" field="StaticFriction" value="0.5000000" type="{EA2C3E90-AFBE-44D4-A90D-FAAF79BAF93D}"/>
<Class name="float" field="Restitution" value="0.7000000" type="{EA2C3E90-AFBE-44D4-A90D-FAAF79BAF93D}"/>
<Class name="unsigned char" field="FrictionCombine" value="3" type="{72B9409A-7D1A-4831-9CFE-FCB3FADD3426}"/>
<Class name="unsigned char" field="RestitutionCombine" value="0" type="{72B9409A-7D1A-4831-9CFE-FCB3FADD3426}"/>
<Class name="float" field="Density" value="2500.0000000" type="{EA2C3E90-AFBE-44D4-A90D-FAAF79BAF93D}"/>
<Class name="Color" field="DebugColor" value="0.4825971 0.8975662 0.9523766 1.0000000" type="{7894072A-9050-4F0F-901B-34B1A0D29417}"/>
</Class>
<Class name="Physics::MaterialId" field="UID" version="1" type="{744CCE6C-9F69-4E2F-B950-DAB8514F870B}">
<Class name="AZ::Uuid" field="MaterialId" value="{FD59CBE9-D1C4-4119-81CB-CD7AD72FC295}" type="{E152C105-A133-4D03-BBF8-3D4B2FBA3E2A}"/>
</Class>
</Class>
<Class name="Physics::MaterialFromAssetConfiguration" field="element" version="1" type="{FBD76628-DE57-435E-BE00-6FFAE64DDF1D}">
<Class name="Physics::MaterialConfiguration" field="Configuration" version="3" type="{8807CAA1-AD08-4238-8FDB-2154ADD084A1}">
<Class name="AZStd::string" field="SurfaceType" value="Metal" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
<Class name="float" field="DynamicFriction" value="0.4200000" type="{EA2C3E90-AFBE-44D4-A90D-FAAF79BAF93D}"/>
<Class name="float" field="StaticFriction" value="0.7800000" type="{EA2C3E90-AFBE-44D4-A90D-FAAF79BAF93D}"/>
<Class name="float" field="Restitution" value="0.4000000" type="{EA2C3E90-AFBE-44D4-A90D-FAAF79BAF93D}"/>
<Class name="unsigned char" field="FrictionCombine" value="0" type="{72B9409A-7D1A-4831-9CFE-FCB3FADD3426}"/>
<Class name="unsigned char" field="RestitutionCombine" value="0" type="{72B9409A-7D1A-4831-9CFE-FCB3FADD3426}"/>
<Class name="float" field="Density" value="1000.0000000" type="{EA2C3E90-AFBE-44D4-A90D-FAAF79BAF93D}"/>
<Class name="Color" field="DebugColor" value="1.0000000 1.0000000 1.0000000 1.0000000" type="{7894072A-9050-4F0F-901B-34B1A0D29417}"/>
<Class name="float" field="Density" value="8050.0000000" type="{EA2C3E90-AFBE-44D4-A90D-FAAF79BAF93D}"/>
<Class name="Color" field="DebugColor" value="0.2312963 0.2312963 0.2312963 1.0000000" type="{7894072A-9050-4F0F-901B-34B1A0D29417}"/>
</Class>
<Class name="Physics::MaterialId" field="UID" version="1" type="{744CCE6C-9F69-4E2F-B950-DAB8514F870B}">
<Class name="AZ::Uuid" field="MaterialId" value="{76CDC778-ACA9-449F-BFD7-C361F89F3207}" type="{E152C105-A133-4D03-BBF8-3D4B2FBA3E2A}"/>
</Class>
</Class>
<Class name="Physics::MaterialFromAssetConfiguration" field="element" version="1" type="{FBD76628-DE57-435E-BE00-6FFAE64DDF1D}">
<Class name="Physics::MaterialConfiguration" field="Configuration" version="3" type="{8807CAA1-AD08-4238-8FDB-2154ADD084A1}">
<Class name="AZStd::string" field="SurfaceType" value="Plastic" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
<Class name="float" field="DynamicFriction" value="0.3500000" type="{EA2C3E90-AFBE-44D4-A90D-FAAF79BAF93D}"/>
<Class name="float" field="StaticFriction" value="0.3000000" type="{EA2C3E90-AFBE-44D4-A90D-FAAF79BAF93D}"/>
<Class name="float" field="Restitution" value="0.6900000" type="{EA2C3E90-AFBE-44D4-A90D-FAAF79BAF93D}"/>
<Class name="unsigned char" field="FrictionCombine" value="3" type="{72B9409A-7D1A-4831-9CFE-FCB3FADD3426}"/>
<Class name="unsigned char" field="RestitutionCombine" value="0" type="{72B9409A-7D1A-4831-9CFE-FCB3FADD3426}"/>
<Class name="float" field="Density" value="900.0000000" type="{EA2C3E90-AFBE-44D4-A90D-FAAF79BAF93D}"/>
<Class name="Color" field="DebugColor" value="0.9394675 1.0000000 0.2735485 1.0000000" type="{7894072A-9050-4F0F-901B-34B1A0D29417}"/>
</Class>
<Class name="Physics::MaterialId" field="UID" version="1" type="{744CCE6C-9F69-4E2F-B950-DAB8514F870B}">
<Class name="AZ::Uuid" field="MaterialId" value="{E2FFB000-D15B-4760-A819-9E490D1D3741}" type="{E152C105-A133-4D03-BBF8-3D4B2FBA3E2A}"/>
</Class>
</Class>
<Class name="Physics::MaterialFromAssetConfiguration" field="element" version="1" type="{FBD76628-DE57-435E-BE00-6FFAE64DDF1D}">
<Class name="Physics::MaterialConfiguration" field="Configuration" version="3" type="{8807CAA1-AD08-4238-8FDB-2154ADD084A1}">
<Class name="AZStd::string" field="SurfaceType" value="Rubber" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
<Class name="float" field="DynamicFriction" value="1.0000000" type="{EA2C3E90-AFBE-44D4-A90D-FAAF79BAF93D}"/>
<Class name="float" field="StaticFriction" value="1.0000000" type="{EA2C3E90-AFBE-44D4-A90D-FAAF79BAF93D}"/>
<Class name="float" field="Restitution" value="0.8500000" type="{EA2C3E90-AFBE-44D4-A90D-FAAF79BAF93D}"/>
<Class name="unsigned char" field="FrictionCombine" value="3" type="{72B9409A-7D1A-4831-9CFE-FCB3FADD3426}"/>
<Class name="unsigned char" field="RestitutionCombine" value="0" type="{72B9409A-7D1A-4831-9CFE-FCB3FADD3426}"/>
<Class name="float" field="Density" value="1200.0000000" type="{EA2C3E90-AFBE-44D4-A90D-FAAF79BAF93D}"/>
<Class name="Color" field="DebugColor" value="0.1088426 0.1088426 0.1088426 1.0000000" type="{7894072A-9050-4F0F-901B-34B1A0D29417}"/>
</Class>
<Class name="Physics::MaterialId" field="UID" version="1" type="{744CCE6C-9F69-4E2F-B950-DAB8514F870B}">
<Class name="AZ::Uuid" field="MaterialId" value="{8C7A6011-61C2-46B7-9BF4-8D4DD2A624F1}" type="{E152C105-A133-4D03-BBF8-3D4B2FBA3E2A}"/>
</Class>
</Class>
<Class name="Physics::MaterialFromAssetConfiguration" field="element" version="1" type="{FBD76628-DE57-435E-BE00-6FFAE64DDF1D}">
<Class name="Physics::MaterialConfiguration" field="Configuration" version="3" type="{8807CAA1-AD08-4238-8FDB-2154ADD084A1}">
<Class name="AZStd::string" field="SurfaceType" value="Terrain_Dirt" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
<Class name="float" field="DynamicFriction" value="0.4000000" type="{EA2C3E90-AFBE-44D4-A90D-FAAF79BAF93D}"/>
<Class name="float" field="StaticFriction" value="0.4000000" type="{EA2C3E90-AFBE-44D4-A90D-FAAF79BAF93D}"/>
<Class name="float" field="Restitution" value="0.3000000" type="{EA2C3E90-AFBE-44D4-A90D-FAAF79BAF93D}"/>
<Class name="unsigned char" field="FrictionCombine" value="0" type="{72B9409A-7D1A-4831-9CFE-FCB3FADD3426}"/>
<Class name="unsigned char" field="RestitutionCombine" value="3" type="{72B9409A-7D1A-4831-9CFE-FCB3FADD3426}"/>
<Class name="float" field="Density" value="1600.0000000" type="{EA2C3E90-AFBE-44D4-A90D-FAAF79BAF93D}"/>
<Class name="Color" field="DebugColor" value="0.3333333 0.2619974 0.1973144 1.0000000" type="{7894072A-9050-4F0F-901B-34B1A0D29417}"/>
</Class>
<Class name="Physics::MaterialId" field="UID" version="1" type="{744CCE6C-9F69-4E2F-B950-DAB8514F870B}">
<Class name="AZ::Uuid" field="MaterialId" value="{303C5A49-22F2-45A8-B24C-9F2C3CA13402}" type="{E152C105-A133-4D03-BBF8-3D4B2FBA3E2A}"/>
</Class>
</Class>
<Class name="Physics::MaterialFromAssetConfiguration" field="element" version="1" type="{FBD76628-DE57-435E-BE00-6FFAE64DDF1D}">
<Class name="Physics::MaterialConfiguration" field="Configuration" version="3" type="{8807CAA1-AD08-4238-8FDB-2154ADD084A1}">
<Class name="AZStd::string" field="SurfaceType" value="Terrain_Grass" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
<Class name="float" field="DynamicFriction" value="0.2500000" type="{EA2C3E90-AFBE-44D4-A90D-FAAF79BAF93D}"/>
<Class name="float" field="StaticFriction" value="0.3500000" type="{EA2C3E90-AFBE-44D4-A90D-FAAF79BAF93D}"/>
<Class name="float" field="Restitution" value="0.2000000" type="{EA2C3E90-AFBE-44D4-A90D-FAAF79BAF93D}"/>
<Class name="unsigned char" field="FrictionCombine" value="3" type="{72B9409A-7D1A-4831-9CFE-FCB3FADD3426}"/>
<Class name="unsigned char" field="RestitutionCombine" value="0" type="{72B9409A-7D1A-4831-9CFE-FCB3FADD3426}"/>
<Class name="float" field="Density" value="1400.0000000" type="{EA2C3E90-AFBE-44D4-A90D-FAAF79BAF93D}"/>
<Class name="Color" field="DebugColor" value="0.1483177 0.5986419 0.1073777 1.0000000" type="{7894072A-9050-4F0F-901B-34B1A0D29417}"/>
</Class>
<Class name="Physics::MaterialId" field="UID" version="1" type="{744CCE6C-9F69-4E2F-B950-DAB8514F870B}">
<Class name="AZ::Uuid" field="MaterialId" value="{53733840-A095-40C4-B653-C40D233B3BE1}" type="{E152C105-A133-4D03-BBF8-3D4B2FBA3E2A}"/>
</Class>
</Class>
<Class name="Physics::MaterialFromAssetConfiguration" field="element" version="1" type="{FBD76628-DE57-435E-BE00-6FFAE64DDF1D}">
<Class name="Physics::MaterialConfiguration" field="Configuration" version="3" type="{8807CAA1-AD08-4238-8FDB-2154ADD084A1}">
<Class name="AZStd::string" field="SurfaceType" value="Vehicle" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
<Class name="float" field="DynamicFriction" value="0.2000000" type="{EA2C3E90-AFBE-44D4-A90D-FAAF79BAF93D}"/>
<Class name="float" field="StaticFriction" value="0.5000000" type="{EA2C3E90-AFBE-44D4-A90D-FAAF79BAF93D}"/>
<Class name="float" field="Restitution" value="0.3000000" type="{EA2C3E90-AFBE-44D4-A90D-FAAF79BAF93D}"/>
<Class name="unsigned char" field="FrictionCombine" value="1" type="{72B9409A-7D1A-4831-9CFE-FCB3FADD3426}"/>
<Class name="unsigned char" field="RestitutionCombine" value="1" type="{72B9409A-7D1A-4831-9CFE-FCB3FADD3426}"/>
<Class name="float" field="Density" value="140.0000000" type="{EA2C3E90-AFBE-44D4-A90D-FAAF79BAF93D}"/>
<Class name="Color" field="DebugColor" value="1.0000000 0.0000000 0.0000000 1.0000000" type="{7894072A-9050-4F0F-901B-34B1A0D29417}"/>
</Class>
<Class name="Physics::MaterialId" field="UID" version="1" type="{744CCE6C-9F69-4E2F-B950-DAB8514F870B}">
<Class name="AZ::Uuid" field="MaterialId" value="{4080A6D4-AF4E-41CE-B7C9-7699C07123E7}" type="{E152C105-A133-4D03-BBF8-3D4B2FBA3E2A}"/>
</Class>
</Class>
<Class name="Physics::MaterialFromAssetConfiguration" field="element" version="1" type="{FBD76628-DE57-435E-BE00-6FFAE64DDF1D}">
<Class name="Physics::MaterialConfiguration" field="Configuration" version="3" type="{8807CAA1-AD08-4238-8FDB-2154ADD084A1}">
<Class name="AZStd::string" field="SurfaceType" value="Wood" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
<Class name="float" field="DynamicFriction" value="0.5000000" type="{EA2C3E90-AFBE-44D4-A90D-FAAF79BAF93D}"/>
<Class name="float" field="StaticFriction" value="0.6000000" type="{EA2C3E90-AFBE-44D4-A90D-FAAF79BAF93D}"/>
<Class name="float" field="Restitution" value="0.6000000" type="{EA2C3E90-AFBE-44D4-A90D-FAAF79BAF93D}"/>
<Class name="unsigned char" field="FrictionCombine" value="3" type="{72B9409A-7D1A-4831-9CFE-FCB3FADD3426}"/>
<Class name="unsigned char" field="RestitutionCombine" value="0" type="{72B9409A-7D1A-4831-9CFE-FCB3FADD3426}"/>
<Class name="float" field="Density" value="540.0000000" type="{EA2C3E90-AFBE-44D4-A90D-FAAF79BAF93D}"/>
<Class name="Color" field="DebugColor" value="1.0000000 0.7318379 0.3004501 1.0000000" type="{7894072A-9050-4F0F-901B-34B1A0D29417}"/>
</Class>
<Class name="Physics::MaterialId" field="UID" version="1" type="{744CCE6C-9F69-4E2F-B950-DAB8514F870B}">
<Class name="AZ::Uuid" field="MaterialId" value="{B072A405-BAFA-4B0A-9164-B3A424E642A9}" type="{E152C105-A133-4D03-BBF8-3D4B2FBA3E2A}"/>
<Class name="AZ::Uuid" field="MaterialId" value="{6ACE67AA-CB32-41CD-8740-58371CCCD3F3}" type="{E152C105-A133-4D03-BBF8-3D4B2FBA3E2A}"/>
</Class>
</Class>
</Class>

@ -2017,8 +2017,8 @@ void CSystem::CreateSystemVars()
REGISTER_CVAR2("sys_streaming_in_blocks", &g_cvars.sys_streaming_in_blocks, 1, VF_NULL,
"Streaming of large files happens in blocks");
#if (defined(WIN32) || defined(WIN64)) && !defined(_RELEASE)
REGISTER_CVAR2("sys_float_exceptions", &g_cvars.sys_float_exceptions, 3, 0, "Use or not use floating point exceptions.");
#if (defined(WIN32) || defined(WIN64)) && defined(_DEBUG)
REGISTER_CVAR2("sys_float_exceptions", &g_cvars.sys_float_exceptions, 2, 0, "Use or not use floating point exceptions.");
#else // Float exceptions by default disabled for console builds.
REGISTER_CVAR2("sys_float_exceptions", &g_cvars.sys_float_exceptions, 0, 0, "Use or not use floating point exceptions.");
#endif

@ -77,6 +77,27 @@
#endif // defined(AZ_ENABLE_DEBUG_TOOLS)
#include <AzCore/Module/Environment.h>
#include <AzCore/std/string/conversions.h>
static void PrintEntityName(const AZ::ConsoleCommandContainer& arguments)
{
if (arguments.empty())
{
return;
}
const auto entityIdStr = AZStd::string(arguments.front());
const auto entityIdValue = AZStd::stoull(entityIdStr);
AZStd::string entityName;
AZ::ComponentApplicationBus::BroadcastResult(
entityName, &AZ::ComponentApplicationBus::Events::GetEntityName, AZ::EntityId(entityIdValue));
AZ_Printf("Entity Debug", "EntityId: %" PRIu64 ", Entity Name: %s", entityIdValue, entityName.c_str());
}
AZ_CONSOLEFREEFUNC(
PrintEntityName, AZ::ConsoleFunctorFlags::Null, "Parameter: EntityId value, Prints the name of the entity to the console");
namespace AZ
{

@ -54,7 +54,7 @@ namespace AzFramework
AZ::Matrix3x4 m_transform = AZ::Matrix3x4::Identity(); //!< Transform to apply to text quads
bool m_monospace = false; //!< disable character proportional spacing
bool m_depthTest = false; //!< Test character against the depth buffer
bool m_virtual800x600ScreenSize = true; //!< Text placement and size are scaled relative to a virtual 800x600 resolution
bool m_virtual800x600ScreenSize = false; //!< Text placement and size are scaled relative to a virtual 800x600 resolution
bool m_scaleWithWindow = false; //!< Font gets bigger as the window gets bigger
bool m_multiline = true; //!< text respects ascii newline characters
};

@ -78,7 +78,7 @@ namespace AzFramework::ProjectManager
projectJsonPath.c_str());
}
if (LaunchProjectManager(engineRootPath))
if (LaunchProjectManager())
{
AZ_TracePrintf("ProjectManager", "Project Manager launched successfully, requesting exit.");
return ProjectPathCheckResult::ProjectManagerLaunched;
@ -87,7 +87,7 @@ namespace AzFramework::ProjectManager
return ProjectPathCheckResult::ProjectManagerLaunchFailed;
}
bool LaunchProjectManager([[maybe_unused]] const AZ::IO::FixedMaxPath& engineRootPath)
bool LaunchProjectManager(const AZStd::string& commandLineArgs)
{
bool launchSuccess = false;
#if (AZ_TRAIT_AZFRAMEWORK_USE_PROJECT_MANAGER)
@ -109,7 +109,7 @@ namespace AzFramework::ProjectManager
}
AzFramework::ProcessLauncher::ProcessLaunchInfo processLaunchInfo;
processLaunchInfo.m_commandlineParameters = executablePath.String();
processLaunchInfo.m_commandlineParameters = executablePath.String() + commandLineArgs;
launchSuccess = AzFramework::ProcessLauncher::LaunchUnwatchedProcess(processLaunchInfo);
}
if (ownsSystemAllocator)

@ -12,6 +12,7 @@
#pragma once
#include <AzCore/IO/Path/Path_fwd.h>
#include <AzCore/std/string/string.h>
namespace AzFramework::ProjectManager
{
@ -21,8 +22,16 @@ namespace AzFramework::ProjectManager
ProjectManagerLaunched = 0,
ProjectPathFound = 1
};
// Check for a project name, if not found, attempts to launch project manager and returns false
//! Check for a project name, if not found, attempts to launch project manager and returns false
//! @param argc the number of arguments in argv
//! @param argv arguments provided to this executable
//! @return a ProjectPathCheckResult
ProjectPathCheckResult CheckProjectPathProvided(const int argc, char* argv[]);
// Attempt to Launch the project manager. Requires locating the engine root, project manager script, and python.
bool LaunchProjectManager(const AZ::IO::FixedMaxPath& engineRootPath);
//! Attempt to Launch the project manager, assuming the o3de executable exists in same folder as
//! current executable. Requires the o3de cli and python.
//! @param commandLineArgs additional command line arguments to provide to the project manager
//! @return true on success, false if failed to find or launch the executable
bool LaunchProjectManager(const AZStd::string& commandLineArgs = "");
} // AzFramework::ProjectManager

@ -12,6 +12,7 @@
#pragma once
#include <AzCore/RTTI/RTTI.h>
#include <AzCore/std/string/string.h>
namespace AzFramework
@ -49,13 +50,17 @@ namespace AzFramework
class ISessionHandlingClientRequests
{
public:
// Handle the player join session process
AZ_RTTI(ISessionHandlingClientRequests, "{41DE6BD3-72BC-4443-BFF9-5B1B9396657A}");
ISessionHandlingClientRequests() = default;
virtual ~ISessionHandlingClientRequests() = default;
// Request the player join session
// @param sessionConnectionConfig The required properties to handle the player join session process
// @return The result of player join session process
virtual bool HandlePlayerJoinSession(const SessionConnectionConfig& sessionConnectionConfig) = 0;
virtual bool RequestPlayerJoinSession(const SessionConnectionConfig& sessionConnectionConfig) = 0;
// Handle the player leave session process
virtual void HandlePlayerLeaveSession() = 0;
// Request the connected player leave session
virtual void RequestPlayerLeaveSession() = 0;
};
//! ISessionHandlingServerRequests
@ -63,6 +68,10 @@ namespace AzFramework
class ISessionHandlingServerRequests
{
public:
AZ_RTTI(ISessionHandlingServerRequests, "{4F0C17BA-F470-4242-A8CB-EC7EA805257C}");
ISessionHandlingServerRequests() = default;
virtual ~ISessionHandlingServerRequests() = default;
// Handle the destroy session process
virtual void HandleDestroySession() = 0;
@ -74,5 +83,10 @@ namespace AzFramework
// Handle the player leave session process
// @param playerConnectionConfig The required properties to handle the player leave session process
virtual void HandlePlayerLeaveSession(const PlayerConnectionConfig& playerConnectionConfig) = 0;
// Retrieves the file location of a pem-encoded TLS certificate
// @return If successful, returns the file location of TLS certificate file; if not successful, returns
// empty string.
virtual AZStd::string GetSessionCertificate() = 0;
};
} // namespace AzFramework

@ -167,6 +167,9 @@ namespace AzFramework
: public AZ::EBusTraits
{
public:
// Safeguard handler for multi-threaded use case
using MutexType = AZStd::recursive_mutex;
//////////////////////////////////////////////////////////////////////////
// EBusTraits overrides
static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Multiple;

@ -24,6 +24,9 @@ namespace AzFramework
: public AZ::EBusTraits
{
public:
// Safeguard handler for multi-threaded use case
using MutexType = AZStd::recursive_mutex;
//////////////////////////////////////////////////////////////////////////
// EBusTraits overrides
static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Multiple;

@ -21,22 +21,6 @@ namespace AzFramework
{
}
Spawnable::Spawnable(Spawnable&& other)
: m_entities(AZStd::move(other.m_entities))
{
}
Spawnable& Spawnable::operator=(Spawnable&& other)
{
if (this != &other)
{
m_entities = AZStd::move(other.m_entities);
}
return *this;
}
const Spawnable::EntityList& Spawnable::GetEntities() const
{
return m_entities;

@ -41,11 +41,11 @@ namespace AzFramework
Spawnable() = default;
explicit Spawnable(const AZ::Data::AssetId& id, AssetStatus status = AssetStatus::NotLoaded);
Spawnable(const Spawnable& rhs) = delete;
Spawnable(Spawnable&& other);
Spawnable(Spawnable&& other) = delete;
~Spawnable() override = default;
Spawnable& operator=(const Spawnable& rhs) = delete;
Spawnable& operator=(Spawnable&& other);
Spawnable& operator=(Spawnable&& other) = delete;
const EntityList& GetEntities() const;
EntityList& GetEntities();

@ -38,19 +38,20 @@ namespace AzFramework
void SpawnableEntitiesContainer::SpawnAllEntities()
{
AZ_Assert(m_threadData, "Calling SpawnAllEntities on a Spawnable container that's not set.");
SpawnableEntitiesInterface::Get()->SpawnAllEntities(m_threadData->m_spawnedEntitiesTicket);
SpawnableEntitiesInterface::Get()->SpawnAllEntities(m_threadData->m_spawnedEntitiesTicket, SpawnablePriority_Default);
}
void SpawnableEntitiesContainer::SpawnEntities(AZStd::vector<size_t> entityIndices)
{
AZ_Assert(m_threadData, "Calling SpawnEntities on a Spawnable container that's not set.");
SpawnableEntitiesInterface::Get()->SpawnEntities(m_threadData->m_spawnedEntitiesTicket, AZStd::move(entityIndices));
SpawnableEntitiesInterface::Get()->SpawnEntities(
m_threadData->m_spawnedEntitiesTicket, SpawnablePriority_Default, AZStd::move(entityIndices));
}
void SpawnableEntitiesContainer::DespawnAllEntities()
{
AZ_Assert(m_threadData, "Calling DespawnEntities on a Spawnable container that's not set.");
SpawnableEntitiesInterface::Get()->DespawnAllEntities(m_threadData->m_spawnedEntitiesTicket);
SpawnableEntitiesInterface::Get()->DespawnAllEntities(m_threadData->m_spawnedEntitiesTicket, SpawnablePriority_Default);
}
void SpawnableEntitiesContainer::Reset(AZ::Data::Asset<Spawnable> spawnable)
@ -66,8 +67,10 @@ namespace AzFramework
m_monitor.Disconnect();
m_monitor.m_threadData.reset();
SpawnableEntitiesInterface::Get()->Barrier(m_threadData->m_spawnedEntitiesTicket,
[threadData = m_threadData](EntitySpawnTicket&) mutable
SpawnableEntitiesInterface::Get()->Barrier(
m_threadData->m_spawnedEntitiesTicket,
SpawnablePriority_Default,
[threadData = m_threadData](EntitySpawnTicket::Id) mutable
{
threadData.reset();
});
@ -83,8 +86,10 @@ namespace AzFramework
void SpawnableEntitiesContainer::Alert(AlertCallback callback)
{
AZ_Assert(m_threadData, "Calling DespawnEntities on a Spawnable container that's not set.");
SpawnableEntitiesInterface::Get()->Barrier(m_threadData->m_spawnedEntitiesTicket,
[generation = m_threadData->m_generation, callback = AZStd::move(callback)](EntitySpawnTicket&)
SpawnableEntitiesInterface::Get()->Barrier(
m_threadData->m_spawnedEntitiesTicket,
SpawnablePriority_Default,
[generation = m_threadData->m_generation, callback = AZStd::move(callback)](EntitySpawnTicket::Id)
{
callback(generation);
});
@ -110,6 +115,7 @@ namespace AzFramework
AZ_Assert(m_threadData, "SpawnableEntitiesContainer is monitoring a spawnable, but doesn't have the associated data.");
AZ_TracePrintf("Spawnables", "Reloading spawnable '%s'.\n", replacementAsset.GetHint().c_str());
SpawnableEntitiesInterface::Get()->ReloadSpawnable(m_threadData->m_spawnedEntitiesTicket, AZStd::move(replacementAsset));
SpawnableEntitiesInterface::Get()->ReloadSpawnable(
m_threadData->m_spawnedEntitiesTicket, SpawnablePriority_Default, AZStd::move(replacementAsset));
}
} // namespace AzFramework

@ -239,7 +239,9 @@ namespace AzFramework
{
auto manager = SpawnableEntitiesInterface::Get();
AZ_Assert(manager, "Attempting to create an entity spawn ticket while the SpawnableEntitiesInterface has no implementation.");
m_payload = manager->CreateTicket(AZStd::move(spawnable));
AZStd::pair<EntitySpawnTicket::Id, void*> result = manager->CreateTicket(AZStd::move(spawnable));
m_id = result.first;
m_payload = result.second;
}
EntitySpawnTicket::~EntitySpawnTicket()
@ -250,6 +252,7 @@ namespace AzFramework
AZ_Assert(manager, "Attempting to destroy an entity spawn ticket while the SpawnableEntitiesInterface has no implementation.");
manager->DestroyTicket(m_payload);
m_payload = nullptr;
m_id = 0;
}
}
@ -263,12 +266,20 @@ namespace AzFramework
AZ_Assert(manager, "Attempting to destroy an entity spawn ticket while the SpawnableEntitiesInterface has no implementation.");
manager->DestroyTicket(m_payload);
}
m_id = rhs.m_id;
rhs.m_id = 0;
m_payload = rhs.m_payload;
rhs.m_payload = nullptr;
}
return *this;
}
auto EntitySpawnTicket::GetId() const -> Id
{
return m_id;
}
bool EntitySpawnTicket::IsValid() const
{
return m_payload != nullptr;

@ -14,6 +14,7 @@
#include <AzCore/Asset/AssetCommon.h>
#include <AzCore/Interface/Interface.h>
#include <AzCore/RTTI/TypeSafeIntegral.h>
#include <AzCore/std/functional.h>
#include <AzFramework/Spawnable/Spawnable.h>
@ -24,6 +25,14 @@ namespace AZ
namespace AzFramework
{
AZ_TYPE_SAFE_INTEGRAL(SpawnablePriority, uint8_t);
inline static constexpr SpawnablePriority SpawnablePriority_Highest { 0 };
inline static constexpr SpawnablePriority SpawnablePriority_High { 32 };
inline static constexpr SpawnablePriority SpawnablePriority_Default { 128 };
inline static constexpr SpawnablePriority SpawnablePriority_Low { 192 };
inline static constexpr SpawnablePriority SpawnablePriority_Lowest { 255 };
class SpawnableEntityContainerView
{
public:
@ -124,16 +133,18 @@ namespace AzFramework
SpawnableIndexEntityIterator m_end;
};
//! Requests to the SpawnableEntitiesInterface require a ticket with a valid spawnable that be used as a template. A ticket can
//! be reused for multiple calls on the same spawnable and is safe to use by multiple threads at the same time. Entities created
//! Requests to the SpawnableEntitiesInterface require a ticket with a valid spawnable that is used as a template. A ticket can
//! be reused for multiple calls on the same spawnable and is safe to be used by multiple threads at the same time. Entities created
//! from the spawnable may be tracked by the ticket and so using the same ticket is needed to despawn the exact entities created
//! by a call so spawn entities. The life cycle of the spawned entities is tied to the ticket and all entities spawned using a
//! by a call to spawn entities. The life cycle of the spawned entities is tied to the ticket and all entities spawned using a
//! ticket will be despawned when it's deleted.
class EntitySpawnTicket
{
public:
friend class SpawnableEntitiesDefinition;
using Id = uint64_t;
EntitySpawnTicket() = default;
EntitySpawnTicket(const EntitySpawnTicket&) = delete;
EntitySpawnTicket(EntitySpawnTicket&& rhs);
@ -143,26 +154,37 @@ namespace AzFramework
EntitySpawnTicket& operator=(const EntitySpawnTicket&) = delete;
EntitySpawnTicket& operator=(EntitySpawnTicket&& rhs);
Id GetId() const;
bool IsValid() const;
private:
void* m_payload{ nullptr };
Id m_id { 0 }; //!< An id that uniquely identifies a ticket.
};
using EntitySpawnCallback = AZStd::function<void(EntitySpawnTicket&, SpawnableConstEntityContainerView)>;
using EntityPreInsertionCallback = AZStd::function<void(EntitySpawnTicket&, SpawnableEntityContainerView)>;
using EntityDespawnCallback = AZStd::function<void(EntitySpawnTicket&)>;
using ReloadSpawnableCallback = AZStd::function<void(EntitySpawnTicket&, SpawnableConstEntityContainerView)>;
using ListEntitiesCallback = AZStd::function<void(EntitySpawnTicket&, SpawnableConstEntityContainerView)>;
using ListIndicesEntitiesCallback = AZStd::function<void(EntitySpawnTicket&, SpawnableConstIndexEntityContainerView)>;
using ClaimEntitiesCallback = AZStd::function<void(EntitySpawnTicket&, SpawnableEntityContainerView)>;
using BarrierCallback = AZStd::function<void(EntitySpawnTicket&)>;
using EntitySpawnCallback = AZStd::function<void(EntitySpawnTicket::Id, SpawnableConstEntityContainerView)>;
using EntityPreInsertionCallback = AZStd::function<void(EntitySpawnTicket::Id, SpawnableEntityContainerView)>;
using EntityDespawnCallback = AZStd::function<void(EntitySpawnTicket::Id)>;
using ReloadSpawnableCallback = AZStd::function<void(EntitySpawnTicket::Id, SpawnableConstEntityContainerView)>;
using ListEntitiesCallback = AZStd::function<void(EntitySpawnTicket::Id, SpawnableConstEntityContainerView)>;
using ListIndicesEntitiesCallback = AZStd::function<void(EntitySpawnTicket::Id, SpawnableConstIndexEntityContainerView)>;
using ClaimEntitiesCallback = AZStd::function<void(EntitySpawnTicket::Id, SpawnableEntityContainerView)>;
using BarrierCallback = AZStd::function<void(EntitySpawnTicket::Id)>;
//! Interface definition to (de)spawn entities from a spawnable into the game world.
//!
//! While the callbacks of the individual calls are being processed they will block processing any other request. Callbacks can be
//! issued from threads other than the one that issued the call, including the main thread.
//!
//! Calls on the same ticket are guaranteed to be executed in the order they are issued. Note that when issuing requests from
//! multiple threads on the same ticket the order in which the requests are assigned to the ticket is not guaranteed.
//!
//! Most calls have a priority with values that range from 0 (highest priority) to 255 (lowest priority). The implementation of this
//! interface may choose to use priority lanes which doesn't guarantee that higher priority requests happen before lower priority
//! requests if they don't pass the priority lane threshold. Priority lanes and their thresholds are implementation specific and may
//! differ between platforms. Note that if a call happened on a ticket with lower priority followed by a one with a higher priority
//! the first lower priority call will still need to complete before the second higher priority call can be executed and the priority
//! of the first call will not be updated.
class SpawnableEntitiesDefinition
{
public:
@ -173,40 +195,48 @@ namespace AzFramework
virtual ~SpawnableEntitiesDefinition() = default;
//! Spawn instances of all entities in the spawnable.
//! @param spawnable The Spawnable asset that will be used to create entity instances from.
//! @param ticket Stores the results of the call. Use this ticket to spawn additional entities or to despawn them.
//! @param priority The priority at which this call will be executed.
//! @param completionCallback Optional callback that's called when spawning entities has completed. This can be called from
//! a different thread than the one that made the function call. The returned list of entities contains all the newly
//! created entities.
virtual void SpawnAllEntities(EntitySpawnTicket& ticket, EntityPreInsertionCallback preInsertionCallback = {},
virtual void SpawnAllEntities(
EntitySpawnTicket& ticket, SpawnablePriority priority, EntityPreInsertionCallback preInsertionCallback = {},
EntitySpawnCallback completionCallback = {}) = 0;
//! Spawn instances of some entities in the spawnable.
//! @param ticket Stores the results of the call. Use this ticket to spawn additional entities or to despawn them.
//! @param priority The priority at which this call will be executed.
//! @param entityIndices The indices into the template entities stored in the spawnable that will be used to spawn entities from.
//! @param completionCallback Optional callback that's called when spawning entities has completed. This can be called from
//! a different thread than the one that made this function call. The returned list of entities contains all the newly
//! created entities.
virtual void SpawnEntities(EntitySpawnTicket& ticket, AZStd::vector<size_t> entityIndices,
virtual void SpawnEntities(
EntitySpawnTicket& ticket, SpawnablePriority priority, AZStd::vector<size_t> entityIndices,
EntityPreInsertionCallback preInsertionCallback = {}, EntitySpawnCallback completionCallback = {}) = 0;
//! Removes all entities in the provided list from the environment.
//! @param ticket The ticket previously used to spawn entities with.
//! @param priority The priority at which this call will be executed.
//! @param completionCallback Optional callback that's called when despawning entities has completed. This can be called from
//! a different thread than the one that made this function call.
virtual void DespawnAllEntities(EntitySpawnTicket& ticket, EntityDespawnCallback completionCallback = {}) = 0;
virtual void DespawnAllEntities(
EntitySpawnTicket& ticket, SpawnablePriority priority, EntityDespawnCallback completionCallback = {}) = 0;
//! Removes all entities in the provided list from the environment and reconstructs the entities from the provided spawnable.
//! @param ticket Stores the results of the call. Use this ticket to spawn additional entities or to despawn them.
//! @param ticket Holds the information on the entities to reload.
//! @param priority The priority at which this call will be executed.
//! @param spawnable The spawnable that will replace the existing spawnable. Both need to have the same asset id.
//! @param completionCallback Optional callback that's called when the entities have been reloaded. This can be called from
//! a different thread than the one that made this function call. The returned list of entities contains all the replacement
//! entities.
virtual void ReloadSpawnable(EntitySpawnTicket& ticket, AZ::Data::Asset<Spawnable> spawnable,
virtual void ReloadSpawnable(
EntitySpawnTicket& ticket, SpawnablePriority priority, AZ::Data::Asset<Spawnable> spawnable,
ReloadSpawnableCallback completionCallback = {}) = 0;
//! List all entities that are spawned using this ticket.
//! @param ticket Only the entities associated with this ticket will be listed.
//! @param priority The priority at which this call will be executed.
//! @param listCallback Required callback that will be called to list the entities on.
virtual void ListEntities(EntitySpawnTicket& ticket, ListEntitiesCallback listCallback) = 0;
virtual void ListEntities(EntitySpawnTicket& ticket, SpawnablePriority priority, ListEntitiesCallback listCallback) = 0;
//! List all entities that are spawned using this ticket with their spawnable index.
//! Spawnables contain a flat list of entities, which are used as templates to spawn entities from. For every spawned entity
//! the index of the entity in the spawnable that was used as a template is stored. This version of ListEntities will return
@ -214,17 +244,23 @@ namespace AzFramework
//! the same index may appear multiple times as there are no restriction on how many instance of a specific entity can be
//! created.
//! @param ticket Only the entities associated with this ticket will be listed.
//! @param priority The priority at which this call will be executed.
//! @param listCallback Required callback that will be called to list the entities and indices on.
virtual void ListIndicesAndEntities(EntitySpawnTicket& ticket, ListIndicesEntitiesCallback listCallback) = 0;
virtual void ListIndicesAndEntities(
EntitySpawnTicket& ticket, SpawnablePriority priority, ListIndicesEntitiesCallback listCallback) = 0;
//! Claim all entities that are spawned using this ticket. Ownership of the entities is transferred from the ticket to the
//! caller through the callback. After this call the ticket will have no entities associated with it. The caller of
//! this function will need to manage the entities after this call.
//! @param ticket Only the entities associated with this ticket will be released.
//! @param priority The priority at which this call will be executed.
//! @param listCallback Required callback that will be called to transfer the entities through.
virtual void ClaimEntities(EntitySpawnTicket& ticket, ClaimEntitiesCallback listCallback) = 0;
virtual void ClaimEntities(EntitySpawnTicket& ticket, SpawnablePriority priority, ClaimEntitiesCallback listCallback) = 0;
//! Blocks until all operations made on the provided ticket before the barrier call have completed.
virtual void Barrier(EntitySpawnTicket& ticket, BarrierCallback completionCallback) = 0;
//! @param ticket The ticket to monitor.
//! @param priority The priority at which this call will be executed.
//! @param completionCallback Required callback that will be called as soon as the barrier has been reached.
virtual void Barrier(EntitySpawnTicket& ticket, SpawnablePriority priority, BarrierCallback completionCallback) = 0;
//! Register a handler for OnSpawned events.
virtual void AddOnSpawnedHandler(AZ::Event<AZ::Data::Asset<Spawnable>>::Handler& handler) = 0;
@ -233,7 +269,7 @@ namespace AzFramework
virtual void AddOnDespawnedHandler(AZ::Event<AZ::Data::Asset<Spawnable>>::Handler& handler) = 0;
protected:
[[nodiscard]] virtual void* CreateTicket(AZ::Data::Asset<Spawnable>&& spawnable) = 0;
[[nodiscard]] virtual AZStd::pair<EntitySpawnTicket::Id, void*> CreateTicket(AZ::Data::Asset<Spawnable>&& spawnable) = 0;
virtual void DestroyTicket(void* ticket) = 0;
template<typename T>

@ -10,9 +10,11 @@
*
*/
#include <AzCore/Casting/numeric_cast.h>
#include <AzCore/Component/ComponentApplicationBus.h>
#include <AzCore/Serialization/IdUtils.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <AzCore/Settings/SettingsRegistry.h>
#include <AzCore/std/parallel/scoped_lock.h>
#include <AzCore/std/smart_ptr/make_shared.h>
#include <AzFramework/Components/TransformComponent.h>
@ -22,128 +24,122 @@
namespace AzFramework
{
void SpawnableEntitiesManager::SpawnAllEntities(EntitySpawnTicket& ticket, EntityPreInsertionCallback preInsertionCallback,
template<typename T>
void SpawnableEntitiesManager::QueueRequest(EntitySpawnTicket& ticket, SpawnablePriority priority, T&& request)
{
request.m_ticket = &GetTicketPayload<Ticket>(ticket);
Queue& queue = priority <= m_highPriorityThreshold ? m_highPriorityQueue : m_regularPriorityQueue;
{
AZStd::scoped_lock queueLock(queue.m_pendingRequestMutex);
request.m_requestId = GetTicketPayload<Ticket>(ticket).m_nextRequestId++;
queue.m_pendingRequest.push(AZStd::move(request));
}
}
SpawnableEntitiesManager::SpawnableEntitiesManager()
{
if (auto settingsRegistry = AZ::SettingsRegistry::Get(); settingsRegistry != nullptr)
{
AZ::u64 value = aznumeric_caster(m_highPriorityThreshold);
settingsRegistry->Get(value, "/O3DE/AzFramework/Spawnables/HighPriorityThreshold");
m_highPriorityThreshold = aznumeric_cast<SpawnablePriority>(AZStd::clamp(value, 0llu, 255llu));
}
}
void SpawnableEntitiesManager::SpawnAllEntities(
EntitySpawnTicket& ticket, SpawnablePriority priority, EntityPreInsertionCallback preInsertionCallback,
EntitySpawnCallback completionCallback)
{
AZ_Assert(ticket.IsValid(), "Ticket provided to SpawnAllEntities hasn't been initialized.");
SpawnAllEntitiesCommand queueEntry;
queueEntry.m_ticket = &ticket;
queueEntry.m_ticketId = ticket.GetId();
queueEntry.m_completionCallback = AZStd::move(completionCallback);
queueEntry.m_preInsertionCallback = AZStd::move(preInsertionCallback);
{
AZStd::scoped_lock queueLock(m_pendingRequestQueueMutex);
queueEntry.m_ticketId = GetTicketPayload<Ticket>(ticket).m_nextTicketId++;
m_pendingRequestQueue.push(AZStd::move(queueEntry));
}
QueueRequest(ticket, priority, AZStd::move(queueEntry));
}
void SpawnableEntitiesManager::SpawnEntities(
EntitySpawnTicket& ticket, AZStd::vector<size_t> entityIndices,
EntitySpawnTicket& ticket, SpawnablePriority priority, AZStd::vector<size_t> entityIndices,
EntityPreInsertionCallback preInsertionCallback, EntitySpawnCallback completionCallback)
{
AZ_Assert(ticket.IsValid(), "Ticket provided to SpawnEntities hasn't been initialized.");
SpawnEntitiesCommand queueEntry;
queueEntry.m_ticket = &ticket;
queueEntry.m_ticketId = ticket.GetId();
queueEntry.m_entityIndices = AZStd::move(entityIndices);
queueEntry.m_completionCallback = AZStd::move(completionCallback);
queueEntry.m_preInsertionCallback = AZStd::move(preInsertionCallback);
{
AZStd::scoped_lock queueLock(m_pendingRequestQueueMutex);
queueEntry.m_ticketId = GetTicketPayload<Ticket>(ticket).m_nextTicketId++;
m_pendingRequestQueue.push(AZStd::move(queueEntry));
}
QueueRequest(ticket, priority, AZStd::move(queueEntry));
}
void SpawnableEntitiesManager::DespawnAllEntities(EntitySpawnTicket& ticket, EntityDespawnCallback completionCallback)
void SpawnableEntitiesManager::DespawnAllEntities(
EntitySpawnTicket& ticket, SpawnablePriority priority, EntityDespawnCallback completionCallback)
{
AZ_Assert(ticket.IsValid(), "Ticket provided to DespawnAllEntities hasn't been initialized.");
DespawnAllEntitiesCommand queueEntry;
queueEntry.m_ticket = &ticket;
queueEntry.m_ticketId = ticket.GetId();
queueEntry.m_completionCallback = AZStd::move(completionCallback);
{
AZStd::scoped_lock queueLock(m_pendingRequestQueueMutex);
queueEntry.m_ticketId = GetTicketPayload<Ticket>(ticket).m_nextTicketId++;
m_pendingRequestQueue.push(AZStd::move(queueEntry));
}
QueueRequest(ticket, priority, AZStd::move(queueEntry));
}
void SpawnableEntitiesManager::ReloadSpawnable(EntitySpawnTicket& ticket, AZ::Data::Asset<Spawnable> spawnable,
void SpawnableEntitiesManager::ReloadSpawnable(
EntitySpawnTicket& ticket, SpawnablePriority priority, AZ::Data::Asset<Spawnable> spawnable,
ReloadSpawnableCallback completionCallback)
{
AZ_Assert(ticket.IsValid(), "Ticket provided to ReloadSpawnable hasn't been initialized.");
ReloadSpawnableCommand queueEntry;
queueEntry.m_ticket = &ticket;
queueEntry.m_ticketId = ticket.GetId();
queueEntry.m_spawnable = AZStd::move(spawnable);
queueEntry.m_completionCallback = AZStd::move(completionCallback);
{
AZStd::scoped_lock queueLock(m_pendingRequestQueueMutex);
queueEntry.m_ticketId = GetTicketPayload<Ticket>(ticket).m_nextTicketId++;
m_pendingRequestQueue.push(AZStd::move(queueEntry));
}
QueueRequest(ticket, priority, AZStd::move(queueEntry));
}
void SpawnableEntitiesManager::ListEntities(EntitySpawnTicket& ticket, ListEntitiesCallback listCallback)
void SpawnableEntitiesManager::ListEntities(EntitySpawnTicket& ticket, SpawnablePriority priority, ListEntitiesCallback listCallback)
{
AZ_Assert(listCallback, "ListEntities called on spawnable entities without a valid callback to use.");
AZ_Assert(ticket.IsValid(), "Ticket provided to ListEntities hasn't been initialized.");
ListEntitiesCommand queueEntry;
queueEntry.m_ticket = &ticket;
queueEntry.m_ticketId = ticket.GetId();
queueEntry.m_listCallback = AZStd::move(listCallback);
{
AZStd::scoped_lock queueLock(m_pendingRequestQueueMutex);
queueEntry.m_ticketId = GetTicketPayload<Ticket>(ticket).m_nextTicketId++;
m_pendingRequestQueue.push(AZStd::move(queueEntry));
}
QueueRequest(ticket, priority, AZStd::move(queueEntry));
}
void SpawnableEntitiesManager::ListIndicesAndEntities(EntitySpawnTicket& ticket, ListIndicesEntitiesCallback listCallback)
void SpawnableEntitiesManager::ListIndicesAndEntities(
EntitySpawnTicket& ticket, SpawnablePriority priority, ListIndicesEntitiesCallback listCallback)
{
AZ_Assert(listCallback, "ListEntities called on spawnable entities without a valid callback to use.");
AZ_Assert(ticket.IsValid(), "Ticket provided to ListEntities hasn't been initialized.");
ListIndicesEntitiesCommand queueEntry;
queueEntry.m_ticket = &ticket;
queueEntry.m_ticketId = ticket.GetId();
queueEntry.m_listCallback = AZStd::move(listCallback);
{
AZStd::scoped_lock queueLock(m_pendingRequestQueueMutex);
queueEntry.m_ticketId = GetTicketPayload<Ticket>(ticket).m_nextTicketId++;
m_pendingRequestQueue.push(AZStd::move(queueEntry));
}
QueueRequest(ticket, priority, AZStd::move(queueEntry));
}
void SpawnableEntitiesManager::ClaimEntities(EntitySpawnTicket& ticket, ClaimEntitiesCallback listCallback)
void SpawnableEntitiesManager::ClaimEntities(EntitySpawnTicket& ticket, SpawnablePriority priority, ClaimEntitiesCallback listCallback)
{
AZ_Assert(listCallback, "ClaimEntities called on spawnable entities without a valid callback to use.");
AZ_Assert(ticket.IsValid(), "Ticket provided to ClaimEntities hasn't been initialized.");
ClaimEntitiesCommand queueEntry;
queueEntry.m_ticket = &ticket;
queueEntry.m_ticketId = ticket.GetId();
queueEntry.m_listCallback = AZStd::move(listCallback);
{
AZStd::scoped_lock queueLock(m_pendingRequestQueueMutex);
queueEntry.m_ticketId = GetTicketPayload<Ticket>(ticket).m_nextTicketId++;
m_pendingRequestQueue.push(AZStd::move(queueEntry));
}
QueueRequest(ticket, priority, AZStd::move(queueEntry));
}
void SpawnableEntitiesManager::Barrier(EntitySpawnTicket& ticket, BarrierCallback completionCallback)
void SpawnableEntitiesManager::Barrier(EntitySpawnTicket& ticket, SpawnablePriority priority, BarrierCallback completionCallback)
{
AZ_Assert(completionCallback, "Barrier on spawnable entities called without a valid callback to use.");
AZ_Assert(ticket.IsValid(), "Ticket provided to Barrier hasn't been initialized.");
BarrierCommand queueEntry;
queueEntry.m_ticket = &ticket;
queueEntry.m_ticketId = ticket.GetId();
queueEntry.m_completionCallback = AZStd::move(completionCallback);
{
AZStd::scoped_lock queueLock(m_pendingRequestQueueMutex);
queueEntry.m_ticketId = GetTicketPayload<Ticket>(ticket).m_nextTicketId++;
m_pendingRequestQueue.push(AZStd::move(queueEntry));
}
QueueRequest(ticket, priority, AZStd::move(queueEntry));
}
void SpawnableEntitiesManager::AddOnSpawnedHandler(AZ::Event<AZ::Data::Asset<Spawnable>>::Handler& handler)
@ -156,34 +152,54 @@ namespace AzFramework
handler.Connect(m_onDespawnedEvent);
}
auto SpawnableEntitiesManager::ProcessQueue() -> CommandQueueStatus
auto SpawnableEntitiesManager::ProcessQueue(CommandQueuePriority priority) -> CommandQueueStatus
{
CommandQueueStatus result = CommandQueueStatus::NoCommandsLeft;
if ((priority & CommandQueuePriority::High) == CommandQueuePriority::High)
{
if (ProcessQueue(m_highPriorityQueue) == CommandQueueStatus::HasCommandsLeft)
{
result = CommandQueueStatus::HasCommandsLeft;
}
}
if ((priority & CommandQueuePriority::Regular) == CommandQueuePriority::Regular)
{
if (ProcessQueue(m_regularPriorityQueue) == CommandQueueStatus::HasCommandsLeft)
{
result = CommandQueueStatus::HasCommandsLeft;
}
}
return result;
}
auto SpawnableEntitiesManager::ProcessQueue(Queue& queue) -> CommandQueueStatus
{
AZStd::queue<Requests> pendingRequestQueue;
{
AZStd::scoped_lock queueLock(m_pendingRequestQueueMutex);
m_pendingRequestQueue.swap(pendingRequestQueue);
AZStd::scoped_lock queueLock(queue.m_pendingRequestMutex);
queue.m_pendingRequest.swap(pendingRequestQueue);
}
if (!pendingRequestQueue.empty() || !m_delayedQueue.empty())
if (!pendingRequestQueue.empty() || !queue.m_delayed.empty())
{
AZ::SerializeContext* serializeContext = nullptr;
AZ::ComponentApplicationBus::BroadcastResult(serializeContext, &AZ::ComponentApplicationBus::Events::GetSerializeContext);
AZ_Assert(serializeContext, "Failed to retrieve serialization context.");
// Only process the requests that are currently in this queue, not the ones that could be re-added if they still can't complete.
size_t delayedSize = m_delayedQueue.size();
size_t delayedSize = queue.m_delayed.size();
for (size_t i = 0; i < delayedSize; ++i)
{
Requests& request = m_delayedQueue.front();
Requests& request = queue.m_delayed.front();
bool result = AZStd::visit([this, serializeContext](auto&& args) -> bool
{
return ProcessRequest(args, *serializeContext);
}, request);
if (!result)
{
m_delayedQueue.emplace_back(AZStd::move(request));
queue.m_delayed.emplace_back(AZStd::move(request));
}
m_delayedQueue.pop_front();
queue.m_delayed.pop_front();
}
do
@ -197,7 +213,7 @@ namespace AzFramework
}, request);
if (!result)
{
m_delayedQueue.emplace_back(AZStd::move(request));
queue.m_delayed.emplace_back(AZStd::move(request));
}
pendingRequestQueue.pop();
}
@ -205,20 +221,22 @@ namespace AzFramework
// Spawning entities can result in more entities being queued to spawn. Repeat spawning until the queue is
// empty to avoid a chain of entity spawning getting dragged out over multiple frames.
{
AZStd::scoped_lock queueLock(m_pendingRequestQueueMutex);
m_pendingRequestQueue.swap(pendingRequestQueue);
AZStd::scoped_lock queueLock(queue.m_pendingRequestMutex);
queue.m_pendingRequest.swap(pendingRequestQueue);
}
} while (!pendingRequestQueue.empty());
}
return m_delayedQueue.empty() ? CommandQueueStatus::NoCommandLeft : CommandQueueStatus::HasCommandsLeft;
return queue.m_delayed.empty() ? CommandQueueStatus::NoCommandsLeft : CommandQueueStatus::HasCommandsLeft;
}
void* SpawnableEntitiesManager::CreateTicket(AZ::Data::Asset<Spawnable>&& spawnable)
AZStd::pair<uint64_t, void*> SpawnableEntitiesManager::CreateTicket(AZ::Data::Asset<Spawnable>&& spawnable)
{
static AZStd::atomic_uint64_t idCounter { 1 };
auto result = aznew Ticket();
result->m_spawnable = AZStd::move(spawnable);
return result;
return AZStd::make_pair<EntitySpawnTicket::Id, void*>(idCounter++, result);
}
void SpawnableEntitiesManager::DestroyTicket(void* ticket)
@ -226,9 +244,9 @@ namespace AzFramework
DestroyTicketCommand queueEntry;
queueEntry.m_ticket = reinterpret_cast<Ticket*>(ticket);
{
AZStd::scoped_lock queueLock(m_pendingRequestQueueMutex);
queueEntry.m_ticketId = reinterpret_cast<Ticket*>(ticket)->m_nextTicketId++;
m_pendingRequestQueue.push(AZStd::move(queueEntry));
AZStd::scoped_lock queueLock(m_regularPriorityQueue.m_pendingRequestMutex);
queueEntry.m_requestId = reinterpret_cast<Ticket*>(ticket)->m_nextRequestId++;
m_regularPriorityQueue.m_pendingRequest.push(AZStd::move(queueEntry));
}
}
@ -251,8 +269,8 @@ namespace AzFramework
bool SpawnableEntitiesManager::ProcessRequest(SpawnAllEntitiesCommand& request, AZ::SerializeContext& serializeContext)
{
Ticket& ticket = GetTicketPayload<Ticket>(*request.m_ticket);
if (ticket.m_spawnable.IsReady() && request.m_ticketId == ticket.m_currentTicketId)
Ticket& ticket = *request.m_ticket;
if (ticket.m_spawnable.IsReady() && request.m_requestId == ticket.m_currentRequestId)
{
AZStd::vector<AZ::Entity*>& spawnedEntities = ticket.m_spawnedEntities;
AZStd::vector<size_t>& spawnedEntityIndices = ticket.m_spawnedEntityIndices;
@ -300,7 +318,7 @@ namespace AzFramework
// Let other systems know about newly spawned entities for any pre-processing before adding to the scene/game context.
if (request.m_preInsertionCallback)
{
request.m_preInsertionCallback(*request.m_ticket, SpawnableEntityContainerView(
request.m_preInsertionCallback(request.m_ticketId, SpawnableEntityContainerView(
ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount, ticket.m_spawnedEntities.end()));
}
@ -314,13 +332,13 @@ namespace AzFramework
// Let other systems know about newly spawned entities for any post-processing after adding to the scene/game context.
if (request.m_completionCallback)
{
request.m_completionCallback(*request.m_ticket, SpawnableConstEntityContainerView(
request.m_completionCallback(request.m_ticketId, SpawnableConstEntityContainerView(
ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount, ticket.m_spawnedEntities.end()));
}
m_onSpawnedEvent.Signal(ticket.m_spawnable);
ticket.m_currentTicketId++;
ticket.m_currentRequestId++;
return true;
}
else
@ -331,8 +349,8 @@ namespace AzFramework
bool SpawnableEntitiesManager::ProcessRequest(SpawnEntitiesCommand& request, AZ::SerializeContext& serializeContext)
{
Ticket& ticket = GetTicketPayload<Ticket>(*request.m_ticket);
if (ticket.m_spawnable.IsReady() && request.m_ticketId == ticket.m_currentTicketId)
Ticket& ticket = *request.m_ticket;
if (ticket.m_spawnable.IsReady() && request.m_requestId == ticket.m_currentRequestId)
{
AZStd::vector<AZ::Entity*>& spawnedEntities = ticket.m_spawnedEntities;
AZStd::vector<size_t>& spawnedEntityIndices = ticket.m_spawnedEntityIndices;
@ -367,9 +385,7 @@ namespace AzFramework
// Let other systems know about newly spawned entities for any pre-processing before adding to the scene/game context.
if (request.m_preInsertionCallback)
{
request.m_preInsertionCallback(
*request.m_ticket,
SpawnableEntityContainerView(
request.m_preInsertionCallback(request.m_ticketId, SpawnableEntityContainerView(
ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount, ticket.m_spawnedEntities.end()));
}
@ -382,13 +398,13 @@ namespace AzFramework
if (request.m_completionCallback)
{
request.m_completionCallback(*request.m_ticket, SpawnableConstEntityContainerView(
request.m_completionCallback(request.m_ticketId, SpawnableConstEntityContainerView(
ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount, ticket.m_spawnedEntities.end()));
}
m_onSpawnedEvent.Signal(ticket.m_spawnable);
ticket.m_currentTicketId++;
ticket.m_currentRequestId++;
return true;
}
else
@ -400,8 +416,8 @@ namespace AzFramework
bool SpawnableEntitiesManager::ProcessRequest(DespawnAllEntitiesCommand& request,
[[maybe_unused]] AZ::SerializeContext& serializeContext)
{
Ticket& ticket = GetTicketPayload<Ticket>(*request.m_ticket);
if (request.m_ticketId == ticket.m_currentTicketId)
Ticket& ticket = *request.m_ticket;
if (request.m_requestId == ticket.m_currentRequestId)
{
for (AZ::Entity* entity : ticket.m_spawnedEntities)
{
@ -417,12 +433,12 @@ namespace AzFramework
if (request.m_completionCallback)
{
request.m_completionCallback(*request.m_ticket);
request.m_completionCallback(request.m_ticketId);
}
m_onDespawnedEvent.Signal(ticket.m_spawnable);
ticket.m_currentTicketId++;
ticket.m_currentRequestId++;
return true;
}
else
@ -433,11 +449,11 @@ namespace AzFramework
bool SpawnableEntitiesManager::ProcessRequest(ReloadSpawnableCommand& request, AZ::SerializeContext& serializeContext)
{
Ticket& ticket = GetTicketPayload<Ticket>(*request.m_ticket);
Ticket& ticket = *request.m_ticket;
AZ_Assert(ticket.m_spawnable.GetId() == request.m_spawnable.GetId(),
"Spawnable is being reloaded, but the provided spawnable has a different asset id. "
"This will likely result in unexpected entities being created.");
if (ticket.m_spawnable.IsReady() && request.m_ticketId == ticket.m_currentTicketId)
if (ticket.m_spawnable.IsReady() && request.m_requestId == ticket.m_currentRequestId)
{
// Delete the original entities.
for (AZ::Entity* entity : ticket.m_spawnedEntities)
@ -493,11 +509,11 @@ namespace AzFramework
if (request.m_completionCallback)
{
request.m_completionCallback(*request.m_ticket, SpawnableConstEntityContainerView(
request.m_completionCallback(request.m_ticketId, SpawnableConstEntityContainerView(
ticket.m_spawnedEntities.begin(), ticket.m_spawnedEntities.end()));
}
ticket.m_currentTicketId++;
ticket.m_currentRequestId++;
m_onSpawnedEvent.Signal(ticket.m_spawnable);
@ -511,12 +527,12 @@ namespace AzFramework
bool SpawnableEntitiesManager::ProcessRequest(ListEntitiesCommand& request, [[maybe_unused]] AZ::SerializeContext& serializeContext)
{
Ticket& ticket = GetTicketPayload<Ticket>(*request.m_ticket);
if (request.m_ticketId == ticket.m_currentTicketId)
Ticket& ticket = *request.m_ticket;
if (request.m_requestId == ticket.m_currentRequestId)
{
request.m_listCallback(*request.m_ticket, SpawnableConstEntityContainerView(
request.m_listCallback(request.m_ticketId, SpawnableConstEntityContainerView(
ticket.m_spawnedEntities.begin(), ticket.m_spawnedEntities.end()));
ticket.m_currentTicketId++;
ticket.m_currentRequestId++;
return true;
}
else
@ -527,17 +543,15 @@ namespace AzFramework
bool SpawnableEntitiesManager::ProcessRequest(ListIndicesEntitiesCommand& request, [[maybe_unused]] AZ::SerializeContext& serializeContext)
{
Ticket& ticket = GetTicketPayload<Ticket>(*request.m_ticket);
if (request.m_ticketId == ticket.m_currentTicketId)
Ticket& ticket = *request.m_ticket;
if (request.m_requestId == ticket.m_currentRequestId)
{
AZ_Assert(
ticket.m_spawnedEntities.size() == ticket.m_spawnedEntityIndices.size(),
"Entities and indices on spawnable ticket have gone out of sync.");
request.m_listCallback(
*request.m_ticket,
SpawnableConstIndexEntityContainerView(
request.m_listCallback(request.m_ticketId, SpawnableConstIndexEntityContainerView(
ticket.m_spawnedEntities.begin(), ticket.m_spawnedEntityIndices.begin(), ticket.m_spawnedEntities.size()));
ticket.m_currentTicketId++;
ticket.m_currentRequestId++;
return true;
}
else
@ -548,16 +562,16 @@ namespace AzFramework
bool SpawnableEntitiesManager::ProcessRequest(ClaimEntitiesCommand& request, [[maybe_unused]] AZ::SerializeContext& serializeContext)
{
Ticket& ticket = GetTicketPayload<Ticket>(*request.m_ticket);
if (request.m_ticketId == ticket.m_currentTicketId)
Ticket& ticket = *request.m_ticket;
if (request.m_requestId == ticket.m_currentRequestId)
{
request.m_listCallback(*request.m_ticket, SpawnableEntityContainerView(
request.m_listCallback(request.m_ticketId, SpawnableEntityContainerView(
ticket.m_spawnedEntities.begin(), ticket.m_spawnedEntities.end()));
ticket.m_spawnedEntities.clear();
ticket.m_spawnedEntityIndices.clear();
ticket.m_currentTicketId++;
ticket.m_currentRequestId++;
return true;
}
else
@ -568,15 +582,15 @@ namespace AzFramework
bool SpawnableEntitiesManager::ProcessRequest(BarrierCommand& request, [[maybe_unused]] AZ::SerializeContext& serializeContext)
{
Ticket& ticket = GetTicketPayload<Ticket>(*request.m_ticket);
if (request.m_ticketId == ticket.m_currentTicketId)
Ticket& ticket = *request.m_ticket;
if (request.m_requestId == ticket.m_currentRequestId)
{
if (request.m_completionCallback)
{
request.m_completionCallback(*request.m_ticket);
request.m_completionCallback(request.m_ticketId);
}
ticket.m_currentTicketId++;
ticket.m_currentRequestId++;
return true;
}
else
@ -587,7 +601,7 @@ namespace AzFramework
bool SpawnableEntitiesManager::ProcessRequest(DestroyTicketCommand& request, [[maybe_unused]] AZ::SerializeContext& serializeContext)
{
if (request.m_ticketId == request.m_ticket->m_currentTicketId)
if (request.m_requestId == request.m_ticket->m_currentRequestId)
{
for (AZ::Entity* entity : request.m_ticket->m_spawnedEntities)
{
@ -606,24 +620,4 @@ namespace AzFramework
return false;
}
}
bool SpawnableEntitiesManager::IsEqualTicket(const EntitySpawnTicket* lhs, const EntitySpawnTicket* rhs)
{
return GetTicketPayload<Ticket>(lhs) == GetTicketPayload<Ticket>(rhs);
}
bool SpawnableEntitiesManager::IsEqualTicket(const Ticket* lhs, const EntitySpawnTicket* rhs)
{
return lhs == GetTicketPayload<Ticket>(rhs);
}
bool SpawnableEntitiesManager::IsEqualTicket(const EntitySpawnTicket* lhs, const Ticket* rhs)
{
return GetTicketPayload<Ticket>(lhs) == rhs;
}
bool SpawnableEntitiesManager::IsEqualTicket(const Ticket* lhs, const Ticket* rhs)
{
return lhs = rhs;
}
} // namespace AzFramework

@ -29,8 +29,6 @@ namespace AZ
namespace AzFramework
{
using EntityIdMap = AZStd::unordered_map<AZ::EntityId, AZ::EntityId>;
class SpawnableEntitiesManager
: public SpawnableEntitiesInterface::Registrar
{
@ -38,31 +36,47 @@ namespace AzFramework
AZ_RTTI(AzFramework::SpawnableEntitiesManager, "{6E14333F-128C-464C-94CA-A63B05A5E51C}");
AZ_CLASS_ALLOCATOR(SpawnableEntitiesManager, AZ::SystemAllocator, 0);
using EntityIdMap = AZStd::unordered_map<AZ::EntityId, AZ::EntityId>;
enum class CommandQueueStatus : bool
{
HasCommandsLeft,
NoCommandLeft
NoCommandsLeft
};
enum class CommandQueuePriority
{
High = 1 << 0,
Regular = 1 << 1
};
SpawnableEntitiesManager();
~SpawnableEntitiesManager() override = default;
//
// The following functions are thread safe
//
void SpawnAllEntities(EntitySpawnTicket& ticket, EntityPreInsertionCallback preInsertionCallback = {}, EntitySpawnCallback completionCallback = {}) override;
void SpawnEntities(EntitySpawnTicket& ticket, AZStd::vector<size_t> entityIndices, EntityPreInsertionCallback preInsertionCallback = {},
void SpawnAllEntities(
EntitySpawnTicket& ticket, SpawnablePriority priority, EntityPreInsertionCallback preInsertionCallback = {},
EntitySpawnCallback completionCallback = {}) override;
void SpawnEntities(
EntitySpawnTicket& ticket, SpawnablePriority priority, AZStd::vector<size_t> entityIndices,
EntityPreInsertionCallback preInsertionCallback = {},
EntitySpawnCallback completionCallback = {}) override;
void DespawnAllEntities(EntitySpawnTicket& ticket, EntityDespawnCallback completionCallback = {}) override;
void DespawnAllEntities(
EntitySpawnTicket& ticket, SpawnablePriority priority, EntityDespawnCallback completionCallback = {}) override;
void ReloadSpawnable(EntitySpawnTicket& ticket, AZ::Data::Asset<Spawnable> spawnable,
void ReloadSpawnable(
EntitySpawnTicket& ticket, SpawnablePriority priority, AZ::Data::Asset<Spawnable> spawnable,
ReloadSpawnableCallback completionCallback = {}) override;
void ListEntities(EntitySpawnTicket& ticket, ListEntitiesCallback listCallback) override;
void ListIndicesAndEntities(EntitySpawnTicket& ticket, ListIndicesEntitiesCallback listCallback) override;
void ClaimEntities(EntitySpawnTicket& ticket, ClaimEntitiesCallback listCallback) override;
void ListEntities(EntitySpawnTicket& ticket, SpawnablePriority priority, ListEntitiesCallback listCallback) override;
void ListIndicesAndEntities(
EntitySpawnTicket& ticket, SpawnablePriority priority, ListIndicesEntitiesCallback listCallback) override;
void ClaimEntities(EntitySpawnTicket& ticket, SpawnablePriority priority, ClaimEntitiesCallback listCallback) override;
void Barrier(EntitySpawnTicket& spawnInfo, BarrierCallback completionCallback) override;
void Barrier(EntitySpawnTicket& spawnInfo, SpawnablePriority priority, BarrierCallback completionCallback) override;
void AddOnSpawnedHandler(AZ::Event<AZ::Data::Asset<Spawnable>>::Handler& handler) override;
void AddOnDespawnedHandler(AZ::Event<AZ::Data::Asset<Spawnable>>::Handler& handler) override;
@ -71,13 +85,9 @@ namespace AzFramework
// The following function is thread safe but intended to be run from the main thread.
//
CommandQueueStatus ProcessQueue();
CommandQueueStatus ProcessQueue(CommandQueuePriority priority);
protected:
void* CreateTicket(AZ::Data::Asset<Spawnable>&& spawnable) override;
void DestroyTicket(void* ticket) override;
private:
struct Ticket
{
AZ_CLASS_ALLOCATOR(Ticket, AZ::ThreadPoolAllocator, 0);
@ -86,8 +96,8 @@ namespace AzFramework
AZStd::vector<AZ::Entity*> m_spawnedEntities;
AZStd::vector<size_t> m_spawnedEntityIndices;
AZ::Data::Asset<Spawnable> m_spawnable;
uint32_t m_nextTicketId{ 0 }; //!< Next id for this ticket.
uint32_t m_currentTicketId{ 0 }; //!< The id for the command that should be executed.
uint32_t m_nextRequestId{ 0 }; //!< Next id for this ticket.
uint32_t m_currentRequestId { 0 }; //!< The id for the command that should be executed.
bool m_loadAll{ true };
};
@ -95,64 +105,86 @@ namespace AzFramework
{
EntitySpawnCallback m_completionCallback;
EntityPreInsertionCallback m_preInsertionCallback;
EntitySpawnTicket* m_ticket;
uint32_t m_ticketId;
Ticket* m_ticket;
EntitySpawnTicket::Id m_ticketId;
uint32_t m_requestId;
};
struct SpawnEntitiesCommand
{
AZStd::vector<size_t> m_entityIndices;
EntitySpawnCallback m_completionCallback;
EntityPreInsertionCallback m_preInsertionCallback;
EntitySpawnTicket* m_ticket;
uint32_t m_ticketId;
Ticket* m_ticket;
EntitySpawnTicket::Id m_ticketId;
uint32_t m_requestId;
};
struct DespawnAllEntitiesCommand
{
EntityDespawnCallback m_completionCallback;
EntitySpawnTicket* m_ticket;
uint32_t m_ticketId;
Ticket* m_ticket;
EntitySpawnTicket::Id m_ticketId;
uint32_t m_requestId;
};
struct ReloadSpawnableCommand
{
AZ::Data::Asset<Spawnable> m_spawnable;
ReloadSpawnableCallback m_completionCallback;
EntitySpawnTicket* m_ticket;
uint32_t m_ticketId;
Ticket* m_ticket;
EntitySpawnTicket::Id m_ticketId;
uint32_t m_requestId;
};
struct ListEntitiesCommand
{
ListEntitiesCallback m_listCallback;
EntitySpawnTicket* m_ticket;
uint32_t m_ticketId;
Ticket* m_ticket;
EntitySpawnTicket::Id m_ticketId;
uint32_t m_requestId;
};
struct ListIndicesEntitiesCommand
{
ListIndicesEntitiesCallback m_listCallback;
EntitySpawnTicket* m_ticket;
uint32_t m_ticketId;
Ticket* m_ticket;
EntitySpawnTicket::Id m_ticketId;
uint32_t m_requestId;
};
struct ClaimEntitiesCommand
{
ClaimEntitiesCallback m_listCallback;
EntitySpawnTicket* m_ticket;
uint32_t m_ticketId;
Ticket* m_ticket;
EntitySpawnTicket::Id m_ticketId;
uint32_t m_requestId;
};
struct BarrierCommand
{
BarrierCallback m_completionCallback;
EntitySpawnTicket* m_ticket;
uint32_t m_ticketId;
Ticket* m_ticket;
EntitySpawnTicket::Id m_ticketId;
uint32_t m_requestId;
};
struct DestroyTicketCommand
{
Ticket* m_ticket;
uint32_t m_ticketId;
uint32_t m_requestId;
};
using Requests = AZStd::variant<
SpawnAllEntitiesCommand, SpawnEntitiesCommand, DespawnAllEntitiesCommand, ReloadSpawnableCommand, ListEntitiesCommand,
ListIndicesEntitiesCommand, ClaimEntitiesCommand, BarrierCommand, DestroyTicketCommand>;
struct Queue
{
AZStd::deque<Requests> m_delayed; //!< Requests that were processed before, but couldn't be completed.
AZStd::queue<Requests> m_pendingRequest; //!< Requests waiting to be processed for the first time.
AZStd::mutex m_pendingRequestMutex;
};
template<typename T>
void QueueRequest(EntitySpawnTicket& ticket, SpawnablePriority priority, T&& request);
AZStd::pair<EntitySpawnTicket::Id, void*> CreateTicket(AZ::Data::Asset<Spawnable>&& spawnable) override;
void DestroyTicket(void* ticket) override;
CommandQueueStatus ProcessQueue(Queue& queue);
AZ::Entity* SpawnSingleEntity(const AZ::Entity& entityTemplate,
AZ::SerializeContext& serializeContext);
@ -169,16 +201,18 @@ namespace AzFramework
bool ProcessRequest(BarrierCommand& request, AZ::SerializeContext& serializeContext);
bool ProcessRequest(DestroyTicketCommand& request, AZ::SerializeContext& serializeContext);
[[nodiscard]] static bool IsEqualTicket(const EntitySpawnTicket* lhs, const EntitySpawnTicket* rhs);
[[nodiscard]] static bool IsEqualTicket(const Ticket* lhs, const EntitySpawnTicket* rhs);
[[nodiscard]] static bool IsEqualTicket(const EntitySpawnTicket* lhs, const Ticket* rhs);
[[nodiscard]] static bool IsEqualTicket(const Ticket* lhs, const Ticket* rhs);
AZStd::deque<Requests> m_delayedQueue; //!< Requests that were processed before, but couldn't be completed.
AZStd::queue<Requests> m_pendingRequestQueue;
AZStd::mutex m_pendingRequestQueueMutex;
Queue m_highPriorityQueue;
Queue m_regularPriorityQueue;
AZ::Event<AZ::Data::Asset<Spawnable>> m_onSpawnedEvent;
AZ::Event<AZ::Data::Asset<Spawnable>> m_onDespawnedEvent;
//! The threshold used to determine if a request goes in the regular (if bigger than the value) or high priority queue (if smaller
//! or equal to this value). The starting value of 64 is chosen as it's between default values SpawnablePriority_High and
//! SpawnablePriority_Default which gives users a bit of room to fine tune the priorities as this value can be configured
//! through the Settings Registry under the key "/O3DE/AzFramework/Spawnables/HighPriorityThreshold".
SpawnablePriority m_highPriorityThreshold { 64 };
};
AZ_DEFINE_ENUM_BITWISE_OPERATORS(AzFramework::SpawnableEntitiesManager::CommandQueuePriority);
} // namespace AzFramework

@ -48,10 +48,23 @@ namespace AzFramework
void SpawnableSystemComponent::OnTick(float /*deltaTime*/, AZ::ScriptTimePoint /*time*/)
{
m_entitiesManager.ProcessQueue();
m_entitiesManager.ProcessQueue(
SpawnableEntitiesManager::CommandQueuePriority::High | SpawnableEntitiesManager::CommandQueuePriority::Regular);
RootSpawnableNotificationBus::ExecuteQueuedEvents();
}
int SpawnableSystemComponent::GetTickOrder()
{
return AZ::ComponentTickBus::TICK_GAME;
}
void SpawnableSystemComponent::OnSystemTick()
{
// Handle only high priority spawning events such as those created from network. These need to happen even if the client
// doesn't have focus to avoid time-out issues for instance.
m_entitiesManager.ProcessQueue(SpawnableEntitiesManager::CommandQueuePriority::High);
}
void SpawnableSystemComponent::OnCatalogLoaded([[maybe_unused]] const char* catalogFile)
{
if (!m_catalogAvailable)
@ -168,7 +181,8 @@ namespace AzFramework
SpawnableEntitiesManager::CommandQueueStatus queueStatus;
do
{
queueStatus = m_entitiesManager.ProcessQueue();
queueStatus = m_entitiesManager.ProcessQueue(
SpawnableEntitiesManager::CommandQueuePriority::High | SpawnableEntitiesManager::CommandQueuePriority::Regular);
} while (queueStatus == SpawnableEntitiesManager::CommandQueueStatus::HasCommandsLeft);
}

@ -28,6 +28,7 @@ namespace AzFramework
class SpawnableSystemComponent
: public AZ::Component
, public AZ::TickBus::Handler
, public AZ::SystemTickBus::Handler
, public AssetCatalogEventBus::Handler
, public RootSpawnableInterface::Registrar
, public RootSpawnableNotificationBus::Handler
@ -58,6 +59,13 @@ namespace AzFramework
//
void OnTick(float deltaTime, AZ::ScriptTimePoint time) override;
int GetTickOrder() override;
//
// SystemTickBus
//
void OnSystemTick() override;
//
// AssetCatalogEventBus

@ -29,7 +29,7 @@ namespace AzFramework
AZ_CVAR(float, ed_cameraSystemOrbitDollyScrollSpeed, 0.02f, nullptr, AZ::ConsoleFunctorFlags::Null, "");
AZ_CVAR(float, ed_cameraSystemOrbitDollyCursorSpeed, 0.01f, nullptr, AZ::ConsoleFunctorFlags::Null, "");
AZ_CVAR(float, ed_cameraSystemScrollTranslateSpeed, 0.02f, nullptr, AZ::ConsoleFunctorFlags::Null, "");
AZ_CVAR(float, ed_cameraSystemMinOrbitDistance, 6.0f, nullptr, AZ::ConsoleFunctorFlags::Null, "");
AZ_CVAR(float, ed_cameraSystemMinOrbitDistance, 10.0f, nullptr, AZ::ConsoleFunctorFlags::Null, "");
AZ_CVAR(float, ed_cameraSystemMaxOrbitDistance, 50.0f, nullptr, AZ::ConsoleFunctorFlags::Null, "");
AZ_CVAR(float, ed_cameraSystemLookSmoothness, 5.0f, nullptr, AZ::ConsoleFunctorFlags::Null, "");
AZ_CVAR(float, ed_cameraSystemTranslateSmoothness, 5.0f, nullptr, AZ::ConsoleFunctorFlags::Null, "");
@ -37,7 +37,6 @@ namespace AzFramework
AZ_CVAR(float, ed_cameraSystemPanSpeed, 0.01f, nullptr, AZ::ConsoleFunctorFlags::Null, "");
AZ_CVAR(bool, ed_cameraSystemPanInvertX, true, nullptr, AZ::ConsoleFunctorFlags::Null, "");
AZ_CVAR(bool, ed_cameraSystemPanInvertY, true, nullptr, AZ::ConsoleFunctorFlags::Null, "");
AZ_CVAR(float, ed_cameraSystemLookDeadzone, 2.0f, nullptr, AZ::ConsoleFunctorFlags::Null, "");
AZ_CVAR(
AZ::CVarFixedString, ed_cameraSystemTranslateForwardKey, "keyboard_key_alphanumeric_W", nullptr, AZ::ConsoleFunctorFlags::Null, "");

@ -303,6 +303,45 @@ namespace AzToolsFramework
return true;
}
bool PrefabLoader::SaveTemplateToFile(TemplateId templateId, AZ::IO::PathView absolutePath)
{
AZ_Assert(absolutePath.IsAbsolute(), "SaveTemplateToFile requires an absolute path for saving the initial prefab file.");
const auto& domAndFilepath = StoreTemplateIntoFileFormat(templateId);
if (!domAndFilepath)
{
return false;
}
// Verify that the absolute path provided to this matches the relative path saved in the template.
// Otherwise, the saved prefab won't be able to be loaded.
auto relativePath = GenerateRelativePath(absolutePath);
if (relativePath != domAndFilepath->second)
{
AZ_Error(
"Prefab", false,
"PrefabLoader::SaveTemplateToFile - "
"Failed to save template '%s' to location '%.*s'."
"Error: Relative path '%.*s' for location didn't match template name.",
domAndFilepath->second.c_str(), AZ_STRING_ARG(absolutePath.Native()), AZ_STRING_ARG(relativePath.Native()));
return false;
}
auto outcome = AzFramework::FileFunc::WriteJsonFile(domAndFilepath->first, absolutePath);
if (!outcome.IsSuccess())
{
AZ_Error(
"Prefab", false,
"PrefabLoader::SaveTemplateToFile - "
"Failed to save template '%s' to location '%.*s'."
"Error: %s",
domAndFilepath->second.c_str(), AZ_STRING_ARG(absolutePath.Native()), outcome.GetError().c_str());
return false;
}
m_prefabSystemComponentInterface->SetTemplateDirtyFlag(templateId, false);
return true;
}
bool PrefabLoader::SaveTemplateToString(TemplateId templateId, AZStd::string& output)
{
const auto& domAndFilepath = StoreTemplateIntoFileFormat(templateId);

@ -72,6 +72,16 @@ namespace AzToolsFramework
*/
bool SaveTemplate(TemplateId templateId) override;
/**
* Saves a Prefab Template to the provided absolute source path, which needs to match the relative path in the template.
* Converts Prefab Template form into .prefab form by collapsing nested Template info
* into a source path and patches.
* @param templateId Id of the template to be saved
* @param absolutePath Absolute path to save the file to
* @return bool on whether the operation succeeded or not
*/
bool SaveTemplateToFile(TemplateId templateId, AZ::IO::PathView absolutePath) override;
/**
* Saves a Prefab Template into the provided output string.
* Converts Prefab Template form into .prefab form by collapsing nested Template info

@ -60,6 +60,16 @@ namespace AzToolsFramework
*/
virtual bool SaveTemplate(TemplateId templateId) = 0;
/**
* Saves a Prefab Template to the provided absolute source path, which needs to match the relative path in the template.
* Converts Prefab Template form into .prefab form by collapsing nested Template info
* into a source path and patches.
* @param templateId Id of the template to be saved
* @param absolutePath Absolute path to save the file to
* @return bool on whether the operation succeeded or not
*/
virtual bool SaveTemplateToFile(TemplateId templateId, AZ::IO::PathView absolutePath) = 0;
/**
* Saves a Prefab Template into the provided output string.
* Converts Prefab Template form into .prefab form by collapsing nested Template info

@ -64,7 +64,7 @@ namespace AzToolsFramework
m_prefabUndoCache.Destroy();
}
PrefabOperationResult PrefabPublicHandler::CreatePrefab(const AZStd::vector<AZ::EntityId>& entityIds, AZ::IO::PathView filePath)
PrefabOperationResult PrefabPublicHandler::CreatePrefab(const AZStd::vector<AZ::EntityId>& entityIds, AZ::IO::PathView absolutePath)
{
EntityList inputEntityList, topLevelEntities;
AZ::EntityId commonRootEntityId;
@ -76,6 +76,8 @@ namespace AzToolsFramework
return findCommonRootOutcome;
}
AZ_Assert(absolutePath.IsAbsolute(), "CreatePrefab requires an absolute path for saving the initial prefab file.");
InstanceOptionalReference instanceToCreate;
{
// Initialize Undo Batch object
@ -144,7 +146,8 @@ namespace AzToolsFramework
// Create the Prefab
instanceToCreate = prefabEditorEntityOwnershipInterface->CreatePrefab(
entities, AZStd::move(instancePtrs), filePath, commonRootEntityOwningInstance);
entities, AZStd::move(instancePtrs), m_prefabLoaderInterface->GenerateRelativePath(absolutePath),
commonRootEntityOwningInstance);
if (!instanceToCreate)
{
@ -254,7 +257,7 @@ namespace AzToolsFramework
}
// Save Template to file
m_prefabLoaderInterface->SaveTemplate(instanceToCreate->get().GetTemplateId());
m_prefabLoaderInterface->SaveTemplateToFile(instanceToCreate->get().GetTemplateId(), absolutePath);
return AZ::Success();
}

@ -46,7 +46,7 @@ namespace AzToolsFramework
void UnregisterPrefabPublicHandlerInterface();
// PrefabPublicInterface...
PrefabOperationResult CreatePrefab(const AZStd::vector<AZ::EntityId>& entityIds, AZ::IO::PathView filePath) override;
PrefabOperationResult CreatePrefab(const AZStd::vector<AZ::EntityId>& entityIds, AZ::IO::PathView absolutePath) override;
PrefabOperationResult InstantiatePrefab(AZStd::string_view filePath, AZ::EntityId parent, const AZ::Vector3& position) override;
PrefabOperationResult SavePrefab(AZ::IO::Path filePath) override;
PrefabEntityResult CreateEntity(AZ::EntityId parentId, const AZ::Vector3& position) override;

@ -46,10 +46,10 @@ namespace AzToolsFramework
* Create a prefab out of the entities provided, at the path provided.
* Automatically detects descendants of entities, and discerns between entities and child instances.
* @param entityIds The entities that should form the new prefab (along with their descendants).
* @param filePath The path for the new prefab file.
* @param filePath The absolute path for the new prefab file.
* @return An outcome object; on failure, it comes with an error message detailing the cause of the error.
*/
virtual PrefabOperationResult CreatePrefab(const AZStd::vector<AZ::EntityId>& entityIds, AZ::IO::PathView filePath) = 0;
virtual PrefabOperationResult CreatePrefab(const AZStd::vector<AZ::EntityId>& entityIds, AZ::IO::PathView absolutePath) = 0;
/**
* Instantiate a prefab from a prefab file.

@ -24,17 +24,6 @@
namespace AzToolsFramework::Prefab::SpawnableUtils
{
AzFramework::Spawnable CreateSpawnable(const PrefabDom& prefabDom)
{
AzFramework::Spawnable spawnable;
AZStd::vector<AZ::Data::Asset<AZ::Data::AssetData>> referencedAssets;
[[maybe_unused]] bool result = CreateSpawnable(spawnable, prefabDom, referencedAssets);
AZ_Assert(result,
"Failed to Load Prefab Instance from given Prefab DOM while Spawnable creation.");
return spawnable;
}
bool CreateSpawnable(AzFramework::Spawnable& spawnable, const PrefabDom& prefabDom)
{
AZStd::vector<AZ::Data::Asset<AZ::Data::AssetData>> referencedAssets;

@ -17,7 +17,6 @@
namespace AzToolsFramework::Prefab::SpawnableUtils
{
AzFramework::Spawnable CreateSpawnable(const PrefabDom& prefabDom);
bool CreateSpawnable(AzFramework::Spawnable& spawnable, const PrefabDom& prefabDom);
bool CreateSpawnable(AzFramework::Spawnable& spawnable, const PrefabDom& prefabDom, AZStd::vector<AZ::Data::Asset<AZ::Data::AssetData>>& referencedAssets);

@ -333,8 +333,7 @@ namespace AzToolsFramework
}
}
auto createPrefabOutcome = s_prefabPublicInterface->CreatePrefab(
selectedEntities, s_prefabLoaderInterface->GenerateRelativePath(prefabFilePath.data()));
auto createPrefabOutcome = s_prefabPublicInterface->CreatePrefab(selectedEntities, prefabFilePath.data());
if (!createPrefabOutcome.IsSuccess())
{

@ -34,7 +34,8 @@ namespace Benchmark
{
state.PauseTiming();
auto spawnable = ::AzToolsFramework::Prefab::SpawnableUtils::CreateSpawnable(prefabDom);
AzFramework::Spawnable spawnable;
AzToolsFramework::Prefab::SpawnableUtils::CreateSpawnable(spawnable, prefabDom);
state.ResumeTiming();
}

@ -40,7 +40,8 @@ namespace UnitTest
//Create Spawnable
auto& prefabDom = m_prefabSystemComponent->FindTemplateDom(instance->GetTemplateId());
auto spawnable = ::AzToolsFramework::Prefab::SpawnableUtils::CreateSpawnable(prefabDom);
AzFramework::Spawnable spawnable;
AzToolsFramework::Prefab::SpawnableUtils::CreateSpawnable(spawnable, prefabDom);
EXPECT_EQ(spawnable.GetEntities().size() - 1, normalEntityCount); // 1 for container entity
const auto& spawnableEntities = spawnable.GetEntities();
@ -84,7 +85,8 @@ namespace UnitTest
//Create Spawnable
auto& prefabDom = m_prefabSystemComponent->FindTemplateDom(thirdInstance->GetTemplateId());
auto spawnable = ::AzToolsFramework::Prefab::SpawnableUtils::CreateSpawnable(prefabDom);
AzFramework::Spawnable spawnable;
AzToolsFramework::Prefab::SpawnableUtils::CreateSpawnable(spawnable, prefabDom);
EXPECT_EQ(spawnable.GetEntities().size() - 1, normalEntityCount); // 1 for container entity
const auto& spawnableEntities = spawnable.GetEntities();

@ -55,7 +55,11 @@ namespace UnitTest
delete m_ticket;
m_ticket = nullptr;
// One more tick on the spawnable entities manager in order to delete the ticket fully.
m_manager->ProcessQueue();
while (m_manager->ProcessQueue(
AzFramework::SpawnableEntitiesManager::CommandQueuePriority::High |
AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular) !=
AzFramework::SpawnableEntitiesManager::CommandQueueStatus::NoCommandsLeft)
;
delete m_spawnableAsset;
m_spawnableAsset = nullptr;
@ -85,6 +89,10 @@ namespace UnitTest
TestApplication* m_application { nullptr };
};
//
// SpawnAllEntitities
//
TEST_F(SpawnableEntitiesManagerTest, SpawnAllEntities_Call_AllEntitiesSpawned)
{
static constexpr size_t NumEntities = 4;
@ -92,16 +100,72 @@ namespace UnitTest
size_t spawnedEntitiesCount = 0;
auto callback =
[&spawnedEntitiesCount](AzFramework::EntitySpawnTicket&, AzFramework::SpawnableConstEntityContainerView entities)
[&spawnedEntitiesCount](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities)
{
spawnedEntitiesCount += entities.size();
};
m_manager->SpawnAllEntities(*m_ticket, {}, AZStd::move(callback));
m_manager->ProcessQueue();
m_manager->SpawnAllEntities(*m_ticket, AzFramework::SpawnablePriority_Default, {}, AZStd::move(callback));
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
EXPECT_EQ(NumEntities, spawnedEntitiesCount);
}
TEST_F(SpawnableEntitiesManagerTest, SpawnAllEntities_DeleteTicketBeforeCall_NoCrash)
{
{
AzFramework::EntitySpawnTicket ticket(*m_spawnableAsset);
m_manager->SpawnAllEntities(ticket, AzFramework::SpawnablePriority_Default);
}
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
}
//
// SpawnEntities
//
TEST_F(SpawnableEntitiesManagerTest, SpawnEntities_DeleteTicketBeforeCall_NoCrash)
{
{
AzFramework::EntitySpawnTicket ticket(*m_spawnableAsset);
m_manager->SpawnEntities(ticket, AzFramework::SpawnablePriority_Default, {});
}
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
}
//
// DespawnAllEntities
//
TEST_F(SpawnableEntitiesManagerTest, DespawnAllEntities_DeleteTicketBeforeCall_NoCrash)
{
{
AzFramework::EntitySpawnTicket ticket(*m_spawnableAsset);
m_manager->DespawnAllEntities(ticket, AzFramework::SpawnablePriority_Default);
}
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
}
//
// ReloadSpawnable
//
TEST_F(SpawnableEntitiesManagerTest, ReloadSpawnable_DeleteTicketBeforeCall_NoCrash)
{
{
AzFramework::EntitySpawnTicket ticket(*m_spawnableAsset);
m_manager->ReloadSpawnable(ticket, AzFramework::SpawnablePriority_Default, *m_spawnableAsset);
}
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
}
//
// ListEntitities
//
TEST_F(SpawnableEntitiesManagerTest, ListEntities_Call_AllEntitiesAreReported)
{
static constexpr size_t NumEntities = 4;
@ -110,7 +174,7 @@ namespace UnitTest
bool allValidEntityIds = true;
size_t spawnedEntitiesCount = 0;
auto callback = [&allValidEntityIds, &spawnedEntitiesCount]
(AzFramework::EntitySpawnTicket&, AzFramework::SpawnableConstEntityContainerView entities)
(AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities)
{
for (auto&& entity : entities)
{
@ -119,14 +183,30 @@ namespace UnitTest
spawnedEntitiesCount += entities.size();
};
m_manager->SpawnAllEntities(*m_ticket);
m_manager->ListEntities(*m_ticket, AZStd::move(callback));
m_manager->ProcessQueue();
m_manager->SpawnAllEntities(*m_ticket, AzFramework::SpawnablePriority_Default);
m_manager->ListEntities(*m_ticket, AzFramework::SpawnablePriority_Default, AZStd::move(callback));
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
EXPECT_TRUE(allValidEntityIds);
EXPECT_EQ(NumEntities, spawnedEntitiesCount);
}
TEST_F(SpawnableEntitiesManagerTest, ListEntities_DeleteTicketBeforeCall_NoCrash)
{
auto callback = [](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView) {};
{
AzFramework::EntitySpawnTicket ticket(*m_spawnableAsset);
m_manager->ListEntities(ticket, AzFramework::SpawnablePriority_Default, AZStd::move(callback));
}
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
}
//
// ListIndicesAndEntities
//
TEST_F(SpawnableEntitiesManagerTest, ListIndicesAndEntities_Call_AllEntitiesAreReportedAndIncrementByOne)
{
static constexpr size_t NumEntities = 4;
@ -135,7 +215,7 @@ namespace UnitTest
bool allValidEntityIds = true;
size_t spawnedEntitiesCount = 0;
auto callback = [&allValidEntityIds, &spawnedEntitiesCount]
(AzFramework::EntitySpawnTicket&, AzFramework::SpawnableConstIndexEntityContainerView entities)
(AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstIndexEntityContainerView entities)
{
for (auto&& indexEntityPair : entities)
{
@ -148,11 +228,121 @@ namespace UnitTest
}
};
m_manager->SpawnAllEntities(*m_ticket);
m_manager->ListIndicesAndEntities(*m_ticket, AZStd::move(callback));
m_manager->ProcessQueue();
m_manager->SpawnAllEntities(*m_ticket, AzFramework::SpawnablePriority_Default);
m_manager->ListIndicesAndEntities(*m_ticket, AzFramework::SpawnablePriority_Default, AZStd::move(callback));
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
EXPECT_TRUE(allValidEntityIds);
EXPECT_EQ(NumEntities, spawnedEntitiesCount);
}
TEST_F(SpawnableEntitiesManagerTest, ListIndicesAndEntities_DeleteTicketBeforeCall_NoCrash)
{
auto callback = [](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstIndexEntityContainerView) {};
{
AzFramework::EntitySpawnTicket ticket(*m_spawnableAsset);
m_manager->ListIndicesAndEntities(ticket, AzFramework::SpawnablePriority_Default, AZStd::move(callback));
}
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
}
//
// ClaimEntities
//
TEST_F(SpawnableEntitiesManagerTest, ClaimEntities_DeleteTicketBeforeCall_NoCrash)
{
auto callback = [](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableEntityContainerView) {};
{
AzFramework::EntitySpawnTicket ticket(*m_spawnableAsset);
m_manager->ClaimEntities(ticket, AzFramework::SpawnablePriority_Default, AZStd::move(callback));
}
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
}
//
// Barrier
//
TEST_F(SpawnableEntitiesManagerTest, Barrier_DeleteTicketBeforeCall_NoCrash)
{
auto callback = [](AzFramework::EntitySpawnTicket::Id) {};
{
AzFramework::EntitySpawnTicket ticket(*m_spawnableAsset);
m_manager->Barrier(ticket, AzFramework::SpawnablePriority_Default, AZStd::move(callback));
}
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
}
//
// Misc. - Priority tests
//
TEST_F(SpawnableEntitiesManagerTest, Priority_HighBeforeDefault_HigherPriorityCallHappensBeforeDefaultPriorityEvenWhenQueuedLater)
{
static constexpr size_t NumEntities = 4;
FillSpawnable(NumEntities);
AzFramework::EntitySpawnTicket highPriorityTicket(*m_spawnableAsset);
size_t callCounter = 1;
size_t highPriorityCallId = 0;
size_t defaultPriorityCallId = 0;
auto highCallback = [&callCounter, &highPriorityCallId]
(AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView)
{
highPriorityCallId = callCounter++;
};
auto defaultCallback = [&callCounter, &defaultPriorityCallId]
(AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView)
{
defaultPriorityCallId = callCounter++;
};
m_manager->SpawnAllEntities(*m_ticket, AzFramework::SpawnablePriority_Default, {}, AZStd::move(defaultCallback));
m_manager->SpawnAllEntities(highPriorityTicket, AzFramework::SpawnablePriority_High, {}, AZStd::move(highCallback));
m_manager->ProcessQueue(
AzFramework::SpawnableEntitiesManager::CommandQueuePriority::High |
AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
EXPECT_LT(highPriorityCallId, defaultPriorityCallId);
}
TEST_F(SpawnableEntitiesManagerTest, Priority_SameTicket_DefaultPriorityCallHappensBeforeHighPriority)
{
static constexpr size_t NumEntities = 4;
FillSpawnable(NumEntities);
size_t callCounter = 1;
size_t highPriorityCallId = 0;
size_t defaultPriorityCallId = 0;
auto highCallback =
[&callCounter, &highPriorityCallId](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView)
{
highPriorityCallId = callCounter++;
};
auto defaultCallback =
[&callCounter, &defaultPriorityCallId](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView)
{
defaultPriorityCallId = callCounter++;
};
m_manager->SpawnAllEntities(*m_ticket, AzFramework::SpawnablePriority_Default, {}, AZStd::move(defaultCallback));
m_manager->SpawnAllEntities(*m_ticket, AzFramework::SpawnablePriority_High, {}, AZStd::move(highCallback));
m_manager->ProcessQueue(
AzFramework::SpawnableEntitiesManager::CommandQueuePriority::High |
AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
// Run a second time as the high priority task will be pending at this point.
m_manager->ProcessQueue(
AzFramework::SpawnableEntitiesManager::CommandQueuePriority::High |
AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
EXPECT_LT(defaultPriorityCallId, highPriorityCallId);
}
} // namespace UnitTest

@ -421,17 +421,18 @@ QMenu* LevelEditorMenuHandler::CreateFileMenu()
fileMenu.AddSeparator();
// Project Settings
auto projectSettingMenu = fileMenu.AddMenu(tr("Project Settings"));
fileMenu.AddAction(ID_FILE_PROJECT_MANAGER_SETTINGS);
// Project Settings Tool
// Platform Settings - Project Settings Tool
// Shortcut must be set while adding the action otherwise it doesn't work
projectSettingMenu.Get()->addAction(
fileMenu.Get()->addAction(
tr(LyViewPane::ProjectSettingsTool),
[]() { QtViewPaneManager::instance()->OpenPane(LyViewPane::ProjectSettingsTool); },
tr("Ctrl+Shift+P"));
projectSettingMenu.AddSeparator();
fileMenu.AddSeparator();
fileMenu.AddAction(ID_FILE_PROJECT_MANAGER_NEW);
fileMenu.AddAction(ID_FILE_PROJECT_MANAGER_OPEN);
fileMenu.AddSeparator();
// NEWMENUS: NEEDS IMPLEMENTATION

@ -58,6 +58,7 @@ AZ_POP_DISABLE_WARNING
#include <AzFramework/Components/CameraBus.h>
#include <AzFramework/StringFunc/StringFunc.h>
#include <AzFramework/Terrain/TerrainDataRequestBus.h>
#include <AzFramework/ProjectManager/ProjectManager.h>
// AzToolsFramework
#include <AzToolsFramework/Component/EditorComponentAPIBus.h>
@ -280,6 +281,8 @@ BOOL CCryDocManager::DoPromptFileName(QString& fileName, [[maybe_unused]] UINT n
[[maybe_unused]] DWORD lFlags, BOOL bOpenFileDialog, [[maybe_unused]] CDocTemplate* pTemplate)
{
CLevelFileDialog levelFileDialog(bOpenFileDialog);
levelFileDialog.show();
levelFileDialog.adjustSize();
if (levelFileDialog.exec() == QDialog::Accepted)
{
@ -477,6 +480,11 @@ void CCryEditApp::RegisterActionHandlers()
ON_COMMAND(ID_FILE_SAVE_LEVEL, OnFileSave)
ON_COMMAND(ID_FILE_EXPORTOCCLUSIONMESH, OnFileExportOcclusionMesh)
// Project Manager
ON_COMMAND(ID_FILE_PROJECT_MANAGER_SETTINGS, OnOpenProjectManagerSettings)
ON_COMMAND(ID_FILE_PROJECT_MANAGER_NEW, OnOpenProjectManagerNew)
ON_COMMAND(ID_FILE_PROJECT_MANAGER_OPEN, OnOpenProjectManager)
}
CCryEditApp* CCryEditApp::s_currentInstance = nullptr;
@ -2073,6 +2081,8 @@ void CCryEditApp::OnDocumentationAWSSupport()
void CCryEditApp::OnDocumentationFeedback()
{
FeedbackDialog dialog;
dialog.show();
dialog.adjustSize();
dialog.exec();
}
@ -2854,6 +2864,34 @@ void CCryEditApp::OnPreferences()
*/
}
void CCryEditApp::OnOpenProjectManagerSettings()
{
OpenProjectManager("UpdateProject");
}
void CCryEditApp::OnOpenProjectManagerNew()
{
OpenProjectManager("CreateProject");
}
void CCryEditApp::OnOpenProjectManager()
{
OpenProjectManager("Projects");
}
void CCryEditApp::OpenProjectManager(const AZStd::string& screen)
{
// provide the current project path for in case we want to update the project
AZ::IO::FixedMaxPathString projectPath = AZ::Utils::GetProjectPath();
const AZStd::string commandLineOptions = AZStd::string::format(" --screen %s --project_path %s", screen.c_str(), projectPath.c_str());
bool launchSuccess = AzFramework::ProjectManager::LaunchProjectManager(commandLineOptions);
if (!launchSuccess)
{
QMessageBox::critical(AzToolsFramework::GetActiveWindow(), QObject::tr("Failed to launch O3DE Project Manager"), QObject::tr("Failed to find or start the O3dE Project Manager"));
}
}
//////////////////////////////////////////////////////////////////////////
void CCryEditApp::OnUndo()
{
@ -3313,6 +3351,8 @@ void CCryEditApp::OnCreateSlice()
void CCryEditApp::OnOpenLevel()
{
CLevelFileDialog levelFileDialog(true);
levelFileDialog.show();
levelFileDialog.adjustSize();
if (levelFileDialog.exec() == QDialog::Accepted)
{

@ -229,6 +229,9 @@ public:
void OnFileResaveSlices();
void OnFileEditEditorini();
void OnPreferences();
void OnOpenProjectManagerSettings();
void OnOpenProjectManagerNew();
void OnOpenProjectManager();
void OnRedo();
void OnUpdateRedo(QAction* action);
void OnUpdateUndo(QAction* action);
@ -366,6 +369,7 @@ private:
AZ_POP_DISABLE_DLL_EXPORT_MEMBER_WARNING
friend struct PythonTestOutputHandler;
void OpenProjectManager(const AZStd::string& screen);
void OnWireframe();
void OnUpdateWireframe(QAction* action);
void OnViewConfigureLayout();

@ -463,16 +463,20 @@ void EditorViewportWidget::Update()
m_renderViewport->GetViewportContext()->SetCameraTransform(LYTransformToAZTransform(m_Camera.GetMatrix()));
}
AZ::Matrix4x4 clipMatrix;
AZ::MakePerspectiveFovMatrixRH(
clipMatrix,
m_Camera.GetFov(),
aznumeric_cast<float>(width()) / aznumeric_cast<float>(height()),
m_Camera.GetNearPlane(),
m_Camera.GetFarPlane(),
true
);
m_renderViewport->GetViewportContext()->SetCameraProjectionMatrix(clipMatrix);
// Don't override the game mode FOV
if (!GetIEditor()->IsInGameMode())
{
AZ::Matrix4x4 clipMatrix;
AZ::MakePerspectiveFovMatrixRH(
clipMatrix,
GetFOV(),
aznumeric_cast<float>(width()) / aznumeric_cast<float>(height()),
m_Camera.GetNearPlane(),
m_Camera.GetFarPlane(),
true
);
m_renderViewport->GetViewportContext()->SetCameraProjectionMatrix(clipMatrix);
}
m_updatingCameraPosition = false;
@ -870,6 +874,13 @@ void EditorViewportWidget::OnBeginPrepareRender()
int w = m_rcClient.width();
int h = m_rcClient.height();
// Don't bother doing an FOV calculation if we don't have a valid viewport
// This prevents frustum calculation bugs with a null viewport
if (w <= 1 || h <= 1)
{
return;
}
float fov = gSettings.viewports.fDefaultFov;
// match viewport fov to default / selected title menu fov
@ -1782,9 +1793,6 @@ void EditorViewportWidget::SetViewTM(const Matrix34& viewTM, bool bMoveOnly)
cameraObject->SetWorldTM(camMatrix * AZMatrix3x3ToLYMatrix3x3(lookThroughEntityCorrection));
}
}
using namespace AzToolsFramework;
ComponentEntityObjectRequestBus::Event(cameraObject, &ComponentEntityObjectRequestBus::Events::UpdatePreemptiveUndoCache);
}
else if (m_viewEntityId.IsValid())
{

@ -30,7 +30,7 @@ namespace LyViewPane
static const char* const EntityInspector = "Entity Inspector";
static const char* const EntityInspectorPinned = "Pinned Entity Inspector";
static const char* const LevelInspector = "Level Inspector";
static const char* const ProjectSettingsTool = "Project Settings Tool";
static const char* const ProjectSettingsTool = "Edit Platform Settings...";
static const char* const ErrorReport = "Error Report";
static const char* const Console = "Console";
static const char* const ConsoleMenuName = "&Console";

@ -748,6 +748,9 @@ void MainWindow::InitActions()
am->AddAction(ID_FILE_EXPORTOCCLUSIONMESH, tr("Export Occlusion Mesh"));
am->AddAction(ID_FILE_EDITLOGFILE, tr("Show Log File"));
am->AddAction(ID_FILE_RESAVESLICES, tr("Resave All Slices"));
am->AddAction(ID_FILE_PROJECT_MANAGER_SETTINGS, tr("Edit Project Settings..."));
am->AddAction(ID_FILE_PROJECT_MANAGER_NEW, tr("New Project..."));
am->AddAction(ID_FILE_PROJECT_MANAGER_OPEN, tr("Open Project..."));
am->AddAction(ID_GAME_PC_ENABLEVERYHIGHSPEC, tr("Very High")).SetCheckable(true)
.RegisterUpdateCallback(cryEdit, &CCryEditApp::OnUpdateGameSpec);
am->AddAction(ID_GAME_PC_ENABLEHIGHSPEC, tr("High")).SetCheckable(true)

@ -313,6 +313,9 @@
#define ID_CREATE_LEVEL_FG_MODULE_FROM_SELECTION 35077
#define ID_GRAPHVIEW_ADD_BLACK_BOX 35078
#define ID_GRAPHVIEW_UNGROUP 35079
#define ID_FILE_PROJECT_MANAGER_NEW 35080
#define ID_FILE_PROJECT_MANAGER_OPEN 35081
#define ID_FILE_PROJECT_MANAGER_SETTINGS 35082
#define ID_TV_TRACKS_TOOLBAR_BASE 35083 // range between ID_TV_TRACKS_TOOLBAR_BASE to ID_TV_TRACKS_TOOLBAR_LAST reserved
#define ID_TV_TRACKS_TOOLBAR_LAST 35183 // for up to 100 "Add Tracks..." dynamically added Track View Track buttons
#define ID_OPEN_TERRAIN_EDITOR 36007

@ -15,14 +15,14 @@ android {
${SIGNING_CONFIGS}
compileSdkVersion sdkVer
buildToolsVersion buildToolsVer
ndkVersion ndkPlatformVer
lintOptions {
abortOnError false
checkReleaseBuilds false
}
defaultConfig {
minSdkVersion ndkPlatformVer
minSdkVersion minSdkVer
targetSdkVersion sdkVer
${NATIVE_CMAKE_SECTION_DEFAULT_CONFIG}
}

@ -16,6 +16,5 @@
# For customization when using a Version Control System, please read the
# header note.
# ${GENERATION_TIMESTAMP}
ndk.dir=${ANDROID_NDK_PATH}
sdk.dir=${ANDROID_SDK_PATH}
${CMAKE_DIR_LINE}

@ -12,10 +12,9 @@ buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.6.4'
classpath 'com.android.tools.build:gradle:${ANDROID_GRADLE_PLUGIN_VERSION}'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
@ -26,14 +25,14 @@ allprojects {
repositories {
google()
jcenter()
}
}
subprojects {
ext {
minSdkVer = ${MIN_SDK_VER}
sdkVer = ${SDK_VER}
ndkPlatformVer = ${NDK_PLATFORM_VER}
ndkPlatformVer = '${NDK_VERSION}'
buildToolsVer = '${SDK_BUILD_TOOL_VER}'
lyEngineRoot = '${LY_ENGINE_ROOT}'
}

@ -67,6 +67,15 @@ namespace O3DE::ProjectManager
return ProjectManagerScreen::CreateProject;
}
void CreateProjectCtrl::NotifyCurrentScreen()
{
ScreenWidget* currentScreen = reinterpret_cast<ScreenWidget*>(m_stack->currentWidget());
if (currentScreen)
{
currentScreen->NotifyCurrentScreen();
}
}
void CreateProjectCtrl::HandleBackButton()
{
if (m_stack->currentIndex() > 0)
@ -110,6 +119,9 @@ namespace O3DE::ProjectManager
auto result = PythonBindingsInterface::Get()->CreateProject(m_projectTemplatePath, m_projectInfo);
if (result.IsSuccess())
{
// automatically register the project
PythonBindingsInterface::Get()->AddProject(m_projectInfo.m_path);
// adding gems is not implemented yet because we don't know what targets to add or how to add them
emit ChangeScreenRequest(ProjectManagerScreen::Projects);
}

@ -31,6 +31,7 @@ namespace O3DE::ProjectManager
explicit CreateProjectCtrl(QWidget* parent = nullptr);
~CreateProjectCtrl() = default;
ProjectManagerScreen GetScreenEnum() override;
void NotifyCurrentScreen() override;
protected slots:
void HandleBackButton();

@ -78,6 +78,14 @@ namespace O3DE::ProjectManager
m_errorLabel->setText(labelText);
}
void FormLineEditWidget::setErrorLabelVisible(bool visible)
{
m_errorLabel->setVisible(visible);
m_frame->setProperty("Valid", !visible);
refreshStyle();
}
QLineEdit* FormLineEditWidget::lineEdit() const
{
return m_lineEdit;

@ -39,6 +39,7 @@ namespace O3DE::ProjectManager
//! Set the error message for to display when invalid.
void setErrorLabelText(const QString& labelText);
void setErrorLabelVisible(bool visible);
//! Returns a pointer to the underlying LineEdit.
QLineEdit* lineEdit() const;

@ -15,6 +15,7 @@
#include <FormLineEditWidget.h>
#include <FormBrowseEditWidget.h>
#include <PathValidator.h>
#include <EngineInfo.h>
#include <QVBoxLayout>
#include <QHBoxLayout>
@ -49,16 +50,16 @@ namespace O3DE::ProjectManager
vLayout->setContentsMargins(0,0,0,0);
vLayout->setAlignment(Qt::AlignTop);
{
m_projectName = new FormLineEditWidget(tr("Project name"), tr("New Project"), this);
m_projectName->setErrorLabelText(
tr("A project with this name already exists at this location. Please choose a new name or location."));
const QString defaultName{ "NewProject" };
const QString defaultPath = QDir::toNativeSeparators(GetDefaultProjectPath() + "/" + defaultName);
m_projectName = new FormLineEditWidget(tr("Project name"), defaultName, this);
connect(m_projectName->lineEdit(), &QLineEdit::textChanged, this, &NewProjectSettingsScreen::ValidateProjectPath);
vLayout->addWidget(m_projectName);
m_projectPath =
new FormBrowseEditWidget(tr("Project Location"), QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation), this);
m_projectPath = new FormBrowseEditWidget(tr("Project Location"), defaultPath, this);
m_projectPath->lineEdit()->setReadOnly(true);
m_projectPath->setErrorLabelText(tr("Please provide a valid path to a folder that exists"));
m_projectPath->lineEdit()->setValidator(new PathValidator(PathValidator::PathMode::ExistingFolder, this));
connect(m_projectPath->lineEdit(), &QLineEdit::textChanged, this, &NewProjectSettingsScreen::ValidateProjectPath);
vLayout->addWidget(m_projectPath);
// if we don't use a QFrame we cannot "contain" the widgets inside and move them around
@ -112,17 +113,41 @@ namespace O3DE::ProjectManager
this->setLayout(hLayout);
}
QString NewProjectSettingsScreen::GetDefaultProjectPath()
{
QString defaultPath = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
AZ::Outcome<EngineInfo> engineInfoResult = PythonBindingsInterface::Get()->GetEngineInfo();
if (engineInfoResult.IsSuccess())
{
QDir path(QDir::toNativeSeparators(engineInfoResult.GetValue().m_defaultProjectsFolder));
if (path.exists())
{
defaultPath = path.absolutePath();
}
}
return defaultPath;
}
ProjectManagerScreen NewProjectSettingsScreen::GetScreenEnum()
{
return ProjectManagerScreen::NewProjectSettings;
}
void NewProjectSettingsScreen::ValidateProjectPath()
{
Validate();
}
void NewProjectSettingsScreen::NotifyCurrentScreen()
{
Validate();
}
ProjectInfo NewProjectSettingsScreen::GetProjectInfo()
{
ProjectInfo projectInfo;
projectInfo.m_projectName = m_projectName->lineEdit()->text();
projectInfo.m_path = QDir::toNativeSeparators(m_projectPath->lineEdit()->text() + "/" + projectInfo.m_projectName);
projectInfo.m_path = m_projectPath->lineEdit()->text();
return projectInfo;
}
@ -133,24 +158,44 @@ namespace O3DE::ProjectManager
bool NewProjectSettingsScreen::Validate()
{
bool projectNameIsValid = true;
if (m_projectName->lineEdit()->text().isEmpty())
{
projectNameIsValid = false;
}
bool projectPathIsValid = true;
if (m_projectPath->lineEdit()->text().isEmpty())
{
projectPathIsValid = false;
m_projectPath->setErrorLabelText(tr("Please provide a valid location."));
}
else
{
QDir path(m_projectPath->lineEdit()->text());
if (path.exists() && !path.isEmpty())
{
projectPathIsValid = false;
m_projectPath->setErrorLabelText(tr("This folder exists and isn't empty. Please choose a different location."));
}
}
QDir path(QDir::toNativeSeparators(m_projectPath->lineEdit()->text() + "/" + m_projectName->lineEdit()->text()));
if (path.exists() && !path.isEmpty())
bool projectNameIsValid = true;
if (m_projectName->lineEdit()->text().isEmpty())
{
projectPathIsValid = false;
projectNameIsValid = false;
m_projectName->setErrorLabelText(tr("Please provide a project name."));
}
else
{
// this validation should roughly match the utils.validate_identifier which the cli
// uses to validate project names
QRegExp validProjectNameRegex("[A-Za-z][A-Za-z0-9_-]{0,63}");
const bool result = validProjectNameRegex.exactMatch(m_projectName->lineEdit()->text());
if (!result)
{
projectNameIsValid = false;
m_projectName->setErrorLabelText(tr("Project names must start with a letter and consist of up to 64 letter, number, '_' or '-' characters"));
}
}
m_projectName->setErrorLabelVisible(!projectNameIsValid);
m_projectPath->setErrorLabelVisible(!projectPathIsValid);
return projectNameIsValid && projectPathIsValid;
}
} // namespace O3DE::ProjectManager

@ -36,10 +36,15 @@ namespace O3DE::ProjectManager
bool Validate();
void NotifyCurrentScreen() override;
protected slots:
void HandleBrowseButton();
void ValidateProjectPath();
private:
QString GetDefaultProjectPath();
FormLineEditWidget* m_projectName;
FormBrowseEditWidget* m_projectPath;
QButtonGroup* m_projectTemplateButtonGroup;

@ -14,13 +14,16 @@
#include <ScreensCtrl.h>
#include <AzQtComponents/Components/StyleManager.h>
#include <AzCore/IO/FileIO.h>
#include <AzCore/IO/Path/Path.h>
#include <AzFramework/CommandLine/CommandLine.h>
#include <AzFramework/Application/Application.h>
#include <QDir>
namespace O3DE::ProjectManager
{
ProjectManagerWindow::ProjectManagerWindow(QWidget* parent, const AZ::IO::PathView& engineRootPath)
ProjectManagerWindow::ProjectManagerWindow(QWidget* parent, const AZ::IO::PathView& engineRootPath, const AZ::IO::PathView& projectPath, ProjectManagerScreen startScreen)
: QMainWindow(parent)
{
m_pythonBindings = AZStd::make_unique<PythonBindings>(engineRootPath);
@ -50,7 +53,18 @@ namespace O3DE::ProjectManager
// set stylesheet after creating the screens or their styles won't get updated
AzQtComponents::StyleManager::setStyleSheet(this, QStringLiteral("style:ProjectManager.qss"));
screensCtrl->ForceChangeToScreen(ProjectManagerScreen::Projects, false);
// always push the projects screen first so we have something to come back to
if (startScreen != ProjectManagerScreen::Projects)
{
screensCtrl->ForceChangeToScreen(ProjectManagerScreen::Projects);
}
screensCtrl->ForceChangeToScreen(startScreen);
if (!projectPath.empty())
{
const QString path = QString::fromUtf8(projectPath.Native().data(), aznumeric_cast<int>(projectPath.Native().size()));
emit screensCtrl->NotifyCurrentProject(path);
}
}
ProjectManagerWindow::~ProjectManagerWindow()

@ -14,6 +14,7 @@
#if !defined(Q_MOC_RUN)
#include <QMainWindow>
#include <PythonBindings.h>
#include <ScreenDefs.h>
#endif
namespace O3DE::ProjectManager
@ -24,7 +25,8 @@ namespace O3DE::ProjectManager
Q_OBJECT
public:
explicit ProjectManagerWindow(QWidget* parent, const AZ::IO::PathView& engineRootPath);
explicit ProjectManagerWindow(QWidget* parent, const AZ::IO::PathView& engineRootPath, const AZ::IO::PathView& projectPath,
ProjectManagerScreen startScreen = ProjectManagerScreen::Projects);
~ProjectManagerWindow();
private:

@ -192,5 +192,16 @@ namespace O3DE::ProjectManager
return true;
}
ProjectManagerScreen GetProjectManagerScreen(const QString& screen)
{
auto iter = s_ProjectManagerStringNames.find(screen);
if (iter != s_ProjectManagerStringNames.end())
{
return iter.value();
}
return ProjectManagerScreen::Invalid;
}
} // namespace ProjectUtils
} // namespace O3DE::ProjectManager

@ -11,6 +11,7 @@
*/
#pragma once
#include <ScreenDefs.h>
#include <QWidget>
namespace O3DE::ProjectManager
@ -24,5 +25,6 @@ namespace O3DE::ProjectManager
bool CopyProject(const QString& origPath, const QString& newPath);
bool DeleteProjectFiles(const QString& path, bool force = false);
bool MoveProject(const QString& origPath, const QString& newPath, QWidget* parent = nullptr);
ProjectManagerScreen GetProjectManagerScreen(const QString& screen);
} // namespace ProjectUtils
} // namespace O3DE::ProjectManager

@ -341,6 +341,16 @@ namespace O3DE::ProjectManager
}
else
{
// refresh the projects content by re-creating it for now
if (m_projectsContent)
{
m_stack->removeWidget(m_projectsContent);
m_projectsContent->deleteLater();
}
m_projectsContent = CreateProjectsContent();
m_stack->addWidget(m_projectsContent);
m_stack->setCurrentWidget(m_projectsContent);
}
}

@ -513,10 +513,15 @@ namespace O3DE::ProjectManager
{
ProjectInfo createdProjectInfo;
bool result = ExecuteWithLock([&] {
pybind11::str projectPath = projectInfo.m_path.toStdString();
pybind11::str projectName = projectInfo.m_projectName.toStdString();
pybind11::str templatePath = projectTemplatePath.toStdString();
auto createProjectResult = m_engineTemplate.attr("create_project")(projectPath, templatePath);
auto createProjectResult = m_engineTemplate.attr("create_project")(
projectPath,
projectName,
templatePath
);
if (createProjectResult.cast<int>() == 0)
{
createdProjectInfo = ProjectInfoFromPath(projectPath);

@ -11,9 +11,13 @@
*/
#pragma once
#include <AzCore/std/containers/unordered_map.h>
#include <AzCore/std/string/string.h>
#include <QHash>
namespace O3DE::ProjectManager
{
enum ProjectManagerScreen
enum class ProjectManagerScreen
{
Invalid = -1,
Empty,
@ -25,4 +29,21 @@ namespace O3DE::ProjectManager
ProjectSettings,
EngineSettings
};
static QHash<QString, ProjectManagerScreen> s_ProjectManagerStringNames = {
{ "Empty", ProjectManagerScreen::Empty},
{ "CreateProject", ProjectManagerScreen::CreateProject},
{ "NewProjectSettings", ProjectManagerScreen::NewProjectSettings},
{ "GemCatalog", ProjectManagerScreen::GemCatalog},
{ "Projects", ProjectManagerScreen::Projects},
{ "UpdateProject", ProjectManagerScreen::UpdateProject},
{ "ProjectSettings", ProjectManagerScreen::ProjectSettings},
{ "EngineSettings", ProjectManagerScreen::EngineSettings}
};
// need to define qHash for ProjectManagerScreen when using scoped enums
inline uint qHash(ProjectManagerScreen key, uint seed)
{
return ::qHash(static_cast<uint>(key), seed);
}
} // namespace O3DE::ProjectManager

@ -136,6 +136,7 @@ namespace O3DE::ProjectManager
{
shouldRestoreCurrentScreen = true;
}
int tabIndex = GetScreenTabIndex(screen);
// Delete old screen if it exists to start fresh
DeleteScreen(screen);
@ -144,11 +145,19 @@ namespace O3DE::ProjectManager
ScreenWidget* newScreen = BuildScreen(this, screen);
if (newScreen->IsTab())
{
m_tabWidget->addTab(newScreen, newScreen->GetTabText());
if (tabIndex > -1)
{
m_tabWidget->insertTab(tabIndex, newScreen, newScreen->GetTabText());
}
else
{
m_tabWidget->addTab(newScreen, newScreen->GetTabText());
}
if (shouldRestoreCurrentScreen)
{
m_tabWidget->setCurrentWidget(newScreen);
m_screenStack->setCurrentWidget(m_tabWidget);
newScreen->NotifyCurrentScreen();
}
}
else
@ -157,6 +166,7 @@ namespace O3DE::ProjectManager
if (shouldRestoreCurrentScreen)
{
m_screenStack->setCurrentWidget(newScreen);
newScreen->NotifyCurrentScreen();
}
}
@ -219,4 +229,19 @@ namespace O3DE::ProjectManager
screen->NotifyCurrentScreen();
}
}
int ScreensCtrl::GetScreenTabIndex(ProjectManagerScreen screen)
{
const auto iter = m_screenMap.find(screen);
if (iter != m_screenMap.end())
{
ScreenWidget* screenWidget = iter.value();
if (screenWidget->IsTab())
{
return m_tabWidget->indexOf(screenWidget);
}
}
return -1;
}
} // namespace O3DE::ProjectManager

@ -51,6 +51,8 @@ namespace O3DE::ProjectManager
void TabChanged(int index);
private:
int GetScreenTabIndex(ProjectManagerScreen screen);
QStackedWidget* m_screenStack;
QHash<ProjectManagerScreen, ScreenWidget*> m_screenMap;
QStack<ProjectManagerScreen> m_screenVisitOrder;

@ -15,13 +15,17 @@
#include <AzCore/Component/ComponentApplication.h>
#include <AzCore/Settings/SettingsRegistryMergeUtils.h>
#include <AzCore/IO/Path/Path.h>
#include <AzFramework/CommandLine/CommandLine.h>
#include <ProjectManagerWindow.h>
#include <ProjectUtils.h>
#include <QApplication>
#include <QCoreApplication>
#include <QGuiApplication>
using namespace O3DE::ProjectManager;
int main(int argc, char* argv[])
{
QApplication::setOrganizationName("O3DE");
@ -51,7 +55,29 @@ int main(int argc, char* argv[])
AzQtComponents::StyleManager styleManager(&app);
styleManager.initialize(&app, engineRootPath);
O3DE::ProjectManager::ProjectManagerWindow window(nullptr, engineRootPath);
// Get the initial start screen if one is provided via command line
constexpr char optionPrefix[] = "--";
AZ::CommandLine commandLine(optionPrefix);
commandLine.Parse(argc, argv);
ProjectManagerScreen startScreen = ProjectManagerScreen::Projects;
if(commandLine.HasSwitch("screen"))
{
QString screenOption = commandLine.GetSwitchValue("screen", 0).c_str();
ProjectManagerScreen screen = ProjectUtils::GetProjectManagerScreen(screenOption);
if (screen != ProjectManagerScreen::Invalid)
{
startScreen = screen;
}
}
AZ::IO::FixedMaxPath projectPath;
if (commandLine.HasSwitch("project-path"))
{
projectPath = commandLine.GetSwitchValue("project-path", 0).c_str();
}
ProjectManagerWindow window(nullptr, engineRootPath, projectPath, startScreen);
window.show();
// somethings is preventing us from moving the window to the center of the

@ -111,8 +111,7 @@ class BatchProcessing:
self._events_firehose_delivery_stream = kinesisfirehose.CfnDeliveryStream(
self._stack,
id='EventsFirehoseDeliveryStream',
delivery_stream_name=f'{self._stack.stack_name}-EventsFirehoseDeliveryStream',
id=f'{self._stack.stack_name}-EventsFirehoseDeliveryStream',
delivery_stream_type='KinesisStreamAsSource',
kinesis_stream_source_configuration=kinesisfirehose.CfnDeliveryStream.KinesisStreamSourceConfigurationProperty(
kinesis_stream_arn=self._input_stream_arn,
@ -327,7 +326,7 @@ class BatchProcessing:
@property
def delivery_stream_name(self) -> kinesisfirehose.CfnDeliveryStream.delivery_stream_name:
return self._events_firehose_delivery_stream.delivery_stream_name
return self._events_firehose_delivery_stream.ref
@property
def delivery_stream_role_arn(self) -> iam.Role.role_arn:

@ -73,14 +73,14 @@ class DataIngestion:
api_id_output = core.CfnOutput(
self._stack,
id='RestApiId',
id='RESTApiId',
description='Service API Id for the analytics pipeline',
export_name=f"{application_name}:RestApiId",
value=self._rest_api.rest_api_id)
stage_output = core.CfnOutput(
self._stack,
id='DeploymentStage',
id='RESTApiStage',
description='Stage for the REST API deployment',
export_name=f"{application_name}:DeploymentStage",
value=self._rest_api.deployment_stage.stage_name)

@ -13,11 +13,6 @@
"displayName": "Metallic",
"description": "Properties for configuring whether the surface is metallic or not."
},
{
"id": "anisotropy",
"displayName": "Anisotropic Material Response",
"description": "How much is this material response anisotropic."
},
{
"id": "roughness",
"displayName": "Roughness",
@ -28,25 +23,25 @@
"displayName": "Specular Reflectance f0",
"description": "The constant f0 represents the specular reflectance at normal incidence (Fresnel 0 Angle). Used to adjust reflectance of non-metal surfaces."
},
{
"id": "clearCoat",
"displayName": "Clear Coat",
"description": "Properties for configuring gloss clear coat"
},
{
"id": "normal",
"displayName": "Normal",
"description": "Properties related to configuring surface normal."
},
{
"id": "opacity",
"displayName": "Opacity",
"description": "Properties for configuring the materials transparency."
"id": "detailLayerGroup",
"displayName": "Detail Layer",
"description": "Properties for Fine Details Layer."
},
{
"id": "uv",
"displayName": "UVs",
"description": "Properties for configuring UV transforms."
"id": "detailUV",
"displayName": "Detail Layer UV",
"description": "Properties for modifying detail layer UV."
},
{
"id": "anisotropy",
"displayName": "Anisotropic Material Response",
"description": "How much is this material response anisotropic."
},
{
"id": "occlusion",
@ -58,25 +53,30 @@
"displayName": "Emissive",
"description": "Properties to add light emission, independent of other lights in the scene."
},
{
"id": "parallax",
"displayName": "Parallax Mapping",
"description": "Properties for parallax effect produced by depthmap."
},
{
"id": "subsurfaceScattering",
"displayName": "Subsurface Scattering",
"description": "Properties for configuring subsurface scattering effects."
},
{
"id": "detailLayerGroup",
"displayName": "Detail Layer",
"description": "Properties for Fine Details Layer."
"id": "clearCoat",
"displayName": "Clear Coat",
"description": "Properties for configuring gloss clear coat"
},
{
"id": "parallax",
"displayName": "Displacement",
"description": "Properties for parallax effect produced by a height map."
},
{
"id": "detailUV",
"displayName": "Detail Layer UV",
"description": "Properties for modifying detail layer UV."
"id": "opacity",
"displayName": "Opacity",
"description": "Properties for configuring the materials transparency."
},
{
"id": "uv",
"displayName": "UVs",
"description": "Properties for configuring UV transforms."
},
{
// Note: this property group is used in the DiffuseGlobalIllumination pass and not by the main forward shader
@ -86,7 +86,7 @@
},
{
"id": "general",
"displayName": "General",
"displayName": "General Settings",
"description": "General settings."
}
],
@ -197,7 +197,7 @@
},
{
"id": "textureMap",
"displayName": "Texture Map",
"displayName": "Texture",
"description": "Base color texture map",
"type": "Image",
"connection": {
@ -208,14 +208,14 @@
{
"id": "useTexture",
"displayName": "Use Texture",
"description": "Whether to use the texture map.",
"description": "Whether to use the texture.",
"type": "Bool",
"defaultValue": true
},
{
"id": "textureMapUv",
"displayName": "UV",
"description": "Base color texture map UV set",
"description": "Base color map UV set",
"type": "Enum",
"enumIsUv": true,
"defaultValue": "Tiled",
@ -227,7 +227,7 @@
{
"id": "textureBlendMode",
"displayName": "Texture Blend Mode",
"description": "Selects the equation to use when combining Color, Factor, and Texture Map.",
"description": "Selects the equation to use when combining Color, Factor, and Texture.",
"type": "Enum",
"enumValues": [ "Multiply", "LinearLight", "Lerp", "Overlay" ],
"defaultValue": "Multiply",
@ -253,7 +253,7 @@
},
{
"id": "textureMap",
"displayName": "Texture Map",
"displayName": "Texture",
"description": "",
"type": "Image",
"connection": {
@ -264,14 +264,14 @@
{
"id": "useTexture",
"displayName": "Use Texture",
"description": "Whether to use the texture map, or just default to the Factor value.",
"description": "Whether to use the texture, or just default to the Factor value.",
"type": "Bool",
"defaultValue": true
},
{
"id": "textureMapUv",
"displayName": "UV",
"description": "Metallic texture map UV set",
"description": "Metallic map UV set",
"type": "Enum",
"enumIsUv": true,
"defaultValue": "Tiled",
@ -284,8 +284,8 @@
"roughness": [
{
"id": "textureMap",
"displayName": "Texture Map",
"description": "Texture map for defining surface roughness.",
"displayName": "Texture",
"description": "Texture for defining surface roughness.",
"type": "Image",
"connection": {
"type": "ShaderInput",
@ -295,14 +295,14 @@
{
"id": "useTexture",
"displayName": "Use Texture",
"description": "Whether to use the texture map, or just default to the Factor value.",
"description": "Whether to use the texture, or just default to the Factor value.",
"type": "Bool",
"defaultValue": true
},
{
"id": "textureMapUv",
"displayName": "UV",
"description": "Roughness texture map UV set",
"description": "Roughness map UV set",
"type": "Enum",
"enumIsUv": true,
"defaultValue": "Tiled",
@ -315,7 +315,7 @@
// Note that "factor" is mutually exclusive with "lowerBound"/"upperBound". These are swapped by a lua functor.
"id": "lowerBound",
"displayName": "Lower Bound",
"description": "The roughness value that corresponds to black in the texture map.",
"description": "The roughness value that corresponds to black in the texture.",
"type": "Float",
"defaultValue": 0.0,
"min": 0.0,
@ -329,7 +329,7 @@
// Note that "factor" is mutually exclusive with "lowerBound"/"upperBound". These are swapped by a lua functor.
"id": "upperBound",
"displayName": "Upper Bound",
"description": "The roughness value that corresponds to white in the texture map.",
"description": "The roughness value that corresponds to white in the texture.",
"type": "Float",
"defaultValue": 1.0,
"min": 0.0,
@ -409,8 +409,8 @@
},
{
"id": "textureMap",
"displayName": "Texture Map",
"description": "Texture map for defining surface reflectance.",
"displayName": "Texture",
"description": "Texture for defining surface reflectance.",
"type": "Image",
"connection": {
"type": "ShaderInput",
@ -420,14 +420,14 @@
{
"id": "useTexture",
"displayName": "Use Texture",
"description": "Whether to use the texture map, or just default to the Factor value.",
"description": "Whether to use the texture, or just default to the Factor value.",
"type": "Bool",
"defaultValue": true
},
{
"id": "textureMapUv",
"displayName": "UV",
"description": "Specular reflection texture map UV set",
"description": "Specular reflection map UV set",
"type": "Enum",
"enumIsUv": true,
"defaultValue": "Tiled",
@ -472,7 +472,7 @@
{
"id": "influenceMap",
"displayName": " Influence Map",
"description": "Strength factor texture map",
"description": "Strength factor texture",
"type": "Image",
"connection": {
"type": "ShaderInput",
@ -482,14 +482,14 @@
{
"id": "useInfluenceMap",
"displayName": " Use Texture",
"description": "Whether to use the texture map, or just default to the Factor value.",
"description": "Whether to use the texture, or just default to the Factor value.",
"type": "Bool",
"defaultValue": true
},
{
"id": "influenceMapUv",
"displayName": " UV",
"description": "Strength factor texture map UV set",
"description": "Strength factor map UV set",
"type": "Enum",
"enumIsUv": true,
"defaultValue": "Tiled",
@ -514,7 +514,7 @@
{
"id": "roughnessMap",
"displayName": " Roughness Map",
"description": "Roughness texture map",
"description": "Texture for defining surface roughness",
"type": "Image",
"connection": {
"type": "ShaderInput",
@ -524,14 +524,14 @@
{
"id": "useRoughnessMap",
"displayName": " Use Texture",
"description": "Whether to use the texture map, or just default to the roughness value.",
"description": "Whether to use the texture, or just default to the roughness value.",
"type": "Bool",
"defaultValue": true
},
{
"id": "roughnessMapUv",
"displayName": " UV",
"description": "Roughness texture map UV set",
"description": "Roughness map UV set",
"type": "Enum",
"enumIsUv": true,
"defaultValue": "Tiled",
@ -573,7 +573,7 @@
{
"id": "normalMapUv",
"displayName": " UV",
"description": "Normal texture map UV set",
"description": "Normal map UV set",
"type": "Enum",
"enumIsUv": true,
"defaultValue": "Tiled",
@ -586,8 +586,8 @@
"normal": [
{
"id": "textureMap",
"displayName": "Texture Map",
"description": "Texture map for defining surface normal direction.",
"displayName": "Texture",
"description": "Texture for defining surface normal direction.",
"type": "Image",
"connection": {
"type": "ShaderInput",
@ -597,14 +597,14 @@
{
"id": "useTexture",
"displayName": "Use Texture",
"description": "Whether to use the texture map, or just rely on vertex normals.",
"description": "Whether to use the texture, or just rely on vertex normals.",
"type": "Bool",
"defaultValue": true
},
{
"id": "textureMapUv",
"displayName": "UV",
"description": "Normal texture map UV set",
"description": "Normal map UV set",
"type": "Enum",
"enumIsUv": true,
"defaultValue": "Tiled",
@ -653,7 +653,7 @@
{
"id": "mode",
"displayName": "Opacity Mode",
"description": "Opacity mode for this texture.",
"description": "Indicates the general approach how transparency is to be applied.",
"type": "Enum",
"enumValues": [ "Opaque", "Cutout", "Blended", "TintedTransparent" ],
"defaultValue": "Opaque",
@ -665,7 +665,7 @@
{
"id": "alphaSource",
"displayName": "Alpha Source",
"description": "Source texture of alpha value.",
"description": "Indicates whether to get the opacity texture from the Base Color map (Packed) or from a separate greyscale texture (Split).",
"type": "Enum",
"enumValues": [ "Packed", "Split", "None" ],
"defaultValue": "Packed",
@ -676,8 +676,8 @@
},
{
"id": "textureMap",
"displayName": "Texture Map",
"description": "Texture map for defining surface opacity.",
"displayName": "Texture",
"description": "Texture for defining surface opacity.",
"type": "Image",
"connection": {
"type": "ShaderInput",
@ -687,7 +687,7 @@
{
"id": "textureMapUv",
"displayName": "UV",
"description": "Opacity texture map UV set",
"description": "Opacity map UV set",
"type": "Enum",
"enumIsUv": true,
"defaultValue": "Tiled",
@ -782,7 +782,7 @@
{
"id": "diffuseTextureMap",
"displayName": "Diffuse AO",
"description": "Texture map for defining occlusion area for diffuse ambient lighting.",
"description": "Texture for defining occlusion area for diffuse ambient lighting.",
"type": "Image",
"connection": {
"type": "ShaderInput",
@ -792,14 +792,14 @@
{
"id": "diffuseUseTexture",
"displayName": " Use Texture",
"description": "Whether to use the Diffuse AO texture map.",
"description": "Whether to use the Diffuse AO map.",
"type": "Bool",
"defaultValue": true
},
{
"id": "diffuseTextureMapUv",
"displayName": " UV",
"description": "Diffuse AO texture map UV set.",
"description": "Diffuse AO map UV set.",
"type": "Enum",
"enumIsUv": true,
"defaultValue": "Tiled",
@ -824,7 +824,7 @@
{
"id": "specularTextureMap",
"displayName": "Specular Cavity",
"description": "Texture map for defining occlusion area for specular lighting.",
"description": "Texture for defining occlusion area for specular lighting.",
"type": "Image",
"connection": {
"type": "ShaderInput",
@ -834,14 +834,14 @@
{
"id": "specularUseTexture",
"displayName": " Use Texture",
"description": "Whether to use the Specular Cavity texture map.",
"description": "Whether to use the Specular Cavity map.",
"type": "Bool",
"defaultValue": true
},
{
"id": "specularTextureMapUv",
"displayName": " UV",
"description": "Specular Cavity texture map UV set.",
"description": "Specular Cavity map UV set.",
"type": "Enum",
"enumIsUv": true,
"defaultValue": "Tiled",
@ -904,8 +904,8 @@
},
{
"id": "textureMap",
"displayName": "Texture Map",
"description": "Texture map for defining emissive area.",
"displayName": "Texture",
"description": "Texture for defining emissive area.",
"type": "Image",
"connection": {
"type": "ShaderInput",
@ -915,14 +915,14 @@
{
"id": "useTexture",
"displayName": "Use Texture",
"description": "Whether to use the texture map.",
"description": "Whether to use the texture.",
"type": "Bool",
"defaultValue": true
},
{
"id": "textureMapUv",
"displayName": "UV",
"description": "Emissive texture map UV set",
"description": "Emissive map UV set",
"type": "Enum",
"enumIsUv": true,
"defaultValue": "Tiled",
@ -935,8 +935,8 @@
"parallax": [
{
"id": "textureMap",
"displayName": "Heightmap",
"description": "Displacement heightmap to create parallax effect.",
"displayName": "Height Map",
"description": "Displacement height map to create parallax effect.",
"type": "Image",
"connection": {
"type": "ShaderInput",
@ -946,14 +946,14 @@
{
"id": "useTexture",
"displayName": "Use Texture",
"description": "Whether to use the heightmap.",
"description": "Whether to use the height map.",
"type": "Bool",
"defaultValue": true
},
{
"id": "textureMapUv",
"displayName": "UV",
"description": "Heightmap UV set",
"description": "Height map UV set",
"type": "Enum",
"enumIsUv": true,
"defaultValue": "Tiled",
@ -964,8 +964,8 @@
},
{
"id": "factor",
"displayName": "Heightmap Scale",
"description": "The total height of the heightmap in local model units.",
"displayName": "Height Map Scale",
"description": "The total height of the height map in local model units.",
"type": "Float",
"defaultValue": 0.05,
"min": 0.0,
@ -1026,7 +1026,7 @@
{
"id": "showClipping",
"displayName": "Show Clipping",
"description": "Highlight areas where the heightmap is clipped by the mesh surface.",
"description": "Highlight areas where the height map is clipped by the mesh surface.",
"type": "Bool",
"defaultValue": false,
"connection": {
@ -1063,7 +1063,7 @@
{
"id": "influenceMap",
"displayName": " Influence Map",
"description": "Use texture map to control the strength of subsurface scattering",
"description": "Texture for controlling the strength of subsurface scattering",
"type": "Image",
"connection": {
"type": "ShaderInput",
@ -1073,7 +1073,7 @@
{
"id": "useInfluenceMap",
"displayName": " Use Influence Map",
"description": "Whether to use the texture map as influence mask.",
"description": "Whether to use the influence map.",
"type": "Bool",
"defaultValue": true
},
@ -1142,7 +1142,7 @@
{
"id": "thicknessMap",
"displayName": " Thickness Map",
"description": "Use a greyscale texture for per pixel thickness",
"description": "Texture for controlling per pixel thickness",
"type": "Image",
"connection": {
"type": "ShaderInput",
@ -1171,7 +1171,7 @@
{
"id": "transmissionTint",
"displayName": " Transmission Tint",
"description": "Color of the volume light travelling through",
"description": "Color of the volume light traveling through",
"type": "Color",
"defaultValue": [ 1.0, 0.8, 0.6 ]
},
@ -1246,7 +1246,7 @@
{
"id": "enableDetailMaskTexture",
"displayName": " Use Texture",
"description": "Enable detail mask texture",
"description": "Enable detail blend mask",
"type": "Bool",
"defaultValue": true
},
@ -1265,7 +1265,7 @@
{
"id": "textureMapUv",
"displayName": "Detail Map UVs",
"description": "Which UV set to use for detail map texture sampling",
"description": "Which UV set to use for detail map sampling",
"type": "Enum",
"enumIsUv": true,
"defaultValue": "Tiled",
@ -1283,8 +1283,8 @@
},
{
"id": "baseColorDetailMap",
"displayName": " Texture Map",
"description": "Detailed Base Color Texture map",
"displayName": " Texture",
"description": "Detailed Base Color Texture",
"type": "Image",
"connection": {
"type": "ShaderInput",
@ -1307,7 +1307,7 @@
{
"id": "enableNormals",
"displayName": "Enable Normal",
"description": "Enable detail normal texture to be used for fine detail normal such as scratches and small dents",
"description": "Enable detail normal map to be used for fine detail normal such as scratches and small dents",
"type": "Bool",
"defaultValue": false
},
@ -1326,8 +1326,8 @@
},
{
"id": "normalDetailMap",
"displayName": " Texture Map",
"description": "Detailed Normal Texture map",
"displayName": " Texture",
"description": "Detailed Normal map",
"type": "Image",
"connection": {
"type": "ShaderInput",
@ -1636,7 +1636,7 @@
{
"type": "Lua",
"args": {
"file": "StandardPBR_SubsurfaceState.lua"
"file": "EnhancedPBR_SubsurfaceState.lua"
}
},
{

@ -23,6 +23,16 @@
"displayName": "Normal",
"description": "Properties related to configuring surface normal."
},
{
"id": "detailLayerGroup",
"displayName": "Detail Layer",
"description": "Properties for Fine Details Layer."
},
{
"id": "detailUV",
"displayName": "Detail Layer UV",
"description": "Properties for modifying detail layer UV."
},
{
"id": "occlusion",
"displayName": "Occlusion",
@ -38,19 +48,9 @@
"displayName": "Wrinkle Layers",
"description": "Properties for wrinkle maps to support morph animation, using vertex color blend weights."
},
{
"id": "detailLayerGroup",
"displayName": "Detail Layer",
"description": "Properties for Fine Details Layer."
},
{
"id": "detailUV",
"displayName": "Detail Layer UV",
"description": "Properties for modifying detail layer UV."
},
{
"id": "general",
"displayName": "General",
"displayName": "General Settings",
"description": "General settings."
}
],
@ -150,7 +150,7 @@
},
{
"id": "textureMap",
"displayName": "Texture Map",
"displayName": "Texture",
"description": "Base color texture map",
"type": "Image",
"connection": {
@ -161,14 +161,14 @@
{
"id": "useTexture",
"displayName": "Use Texture",
"description": "Whether to use the texture map.",
"description": "Whether to use the texture.",
"type": "Bool",
"defaultValue": true
},
{
"id": "textureMapUv",
"displayName": "UV",
"description": "Base color texture map UV set",
"description": "Base color map UV set",
"type": "Enum",
"enumIsUv": true,
"defaultValue": "Unwrapped",
@ -180,7 +180,7 @@
{
"id": "textureBlendMode",
"displayName": "Texture Blend Mode",
"description": "Selects the equation to use when combining Color, Factor, and Texture Map.",
"description": "Selects the equation to use when combining Color, Factor, and Texture.",
"type": "Enum",
"enumValues": [ "Multiply", "LinearLight", "Lerp", "Overlay" ],
"defaultValue": "Multiply",
@ -193,8 +193,8 @@
"roughness": [
{
"id": "textureMap",
"displayName": "Texture Map",
"description": "Texture map for defining surface roughness.",
"displayName": "Texture",
"description": "Texture for defining surface roughness.",
"type": "Image",
"connection": {
"type": "ShaderInput",
@ -204,14 +204,14 @@
{
"id": "useTexture",
"displayName": "Use Texture",
"description": "Whether to use the texture map, or just default to the Factor value.",
"description": "Whether to use the texture, or just default to the Factor value.",
"type": "Bool",
"defaultValue": true
},
{
"id": "textureMapUv",
"displayName": "UV",
"description": "Roughness texture map UV set",
"description": "Roughness map UV set",
"type": "Enum",
"enumIsUv": true,
"defaultValue": "Unwrapped",
@ -224,7 +224,7 @@
// Note that "factor" is mutually exclusive with "lowerBound"/"upperBound". These are swapped by a lua functor.
"id": "lowerBound",
"displayName": "Lower Bound",
"description": "The roughness value that corresponds to black in the texture map.",
"description": "The roughness value that corresponds to black in the texture.",
"type": "Float",
"defaultValue": 0.0,
"min": 0.0,
@ -238,7 +238,7 @@
// Note that "factor" is mutually exclusive with "lowerBound"/"upperBound". These are swapped by a lua functor.
"id": "upperBound",
"displayName": "Upper Bound",
"description": "The roughness value that corresponds to white in the texture map.",
"description": "The roughness value that corresponds to white in the texture.",
"type": "Float",
"defaultValue": 1.0,
"min": 0.0,
@ -279,8 +279,8 @@
},
{
"id": "textureMap",
"displayName": "Texture Map",
"description": "Texture map for defining surface reflectance.",
"displayName": "Texture",
"description": "Texture for defining surface reflectance.",
"type": "Image",
"connection": {
"type": "ShaderInput",
@ -290,14 +290,14 @@
{
"id": "useTexture",
"displayName": "Use Texture",
"description": "Whether to use the texture map, or just default to the Factor value.",
"description": "Whether to use the texture, or just default to the Factor value.",
"type": "Bool",
"defaultValue": true
},
{
"id": "textureMapUv",
"displayName": "UV",
"description": "Specular reflection texture map UV set",
"description": "Specular reflection map UV set",
"type": "Enum",
"enumIsUv": true,
"defaultValue": "Unwrapped",
@ -321,8 +321,8 @@
"normal": [
{
"id": "textureMap",
"displayName": "Texture Map",
"description": "Texture map for defining surface normal direction.",
"displayName": "Texture",
"description": "Texture for defining surface normal direction.",
"type": "Image",
"connection": {
"type": "ShaderInput",
@ -332,14 +332,14 @@
{
"id": "useTexture",
"displayName": "Use Texture",
"description": "Whether to use the texture map, or just rely on vertex normals.",
"description": "Whether to use the texture, or just rely on vertex normals.",
"type": "Bool",
"defaultValue": true
},
{
"id": "textureMapUv",
"displayName": "UV",
"description": "Normal texture map UV set",
"description": "Normal map UV set",
"type": "Enum",
"enumIsUv": true,
"defaultValue": "Unwrapped",
@ -388,7 +388,7 @@
{
"id": "diffuseTextureMap",
"displayName": "Diffuse AO",
"description": "Texture map for defining occlusion area for diffuse ambient lighting.",
"description": "Texture for defining occlusion area for diffuse ambient lighting.",
"type": "Image",
"connection": {
"type": "ShaderInput",
@ -398,14 +398,14 @@
{
"id": "diffuseUseTexture",
"displayName": " Use Texture",
"description": "Whether to use the Diffuse AO texture map.",
"description": "Whether to use the Diffuse AO map.",
"type": "Bool",
"defaultValue": true
},
{
"id": "diffuseTextureMapUv",
"displayName": " UV",
"description": "Diffuse AO texture map UV set.",
"description": "Diffuse AO map UV set.",
"type": "Enum",
"enumIsUv": true,
"defaultValue": "Tiled",
@ -430,7 +430,7 @@
{
"id": "specularTextureMap",
"displayName": "Specular Cavity",
"description": "Texture map for defining occlusion area for specular lighting.",
"description": "Texture for defining occlusion area for specular lighting.",
"type": "Image",
"connection": {
"type": "ShaderInput",
@ -440,14 +440,14 @@
{
"id": "specularUseTexture",
"displayName": " Use Texture",
"description": "Whether to use the Specular Cavity texture map.",
"description": "Whether to use the Specular Cavity map.",
"type": "Bool",
"defaultValue": true
},
{
"id": "specularTextureMapUv",
"displayName": " UV",
"description": "Specular Cavity texture map UV set.",
"description": "Specular Cavity map UV set.",
"type": "Enum",
"enumIsUv": true,
"defaultValue": "Tiled",
@ -498,7 +498,7 @@
{
"id": "influenceMap",
"displayName": " Influence Map",
"description": "Use texture map to control the strength of subsurface scattering",
"description": "Texture for controlling the strength of subsurface scattering",
"type": "Image",
"connection": {
"type": "ShaderInput",
@ -508,7 +508,7 @@
{
"id": "useInfluenceMap",
"displayName": " Use Influence Map",
"description": "Whether to use the texture map as influence mask.",
"description": "Whether to use the influence map.",
"type": "Bool",
"defaultValue": true
},
@ -577,7 +577,7 @@
{
"id": "thicknessMap",
"displayName": " Thickness Map",
"description": "Use a greyscale texture for per pixel thickness",
"description": "Texture for controlling per pixel thickness",
"type": "Image",
"connection": {
"type": "ShaderInput",
@ -606,7 +606,7 @@
{
"id": "transmissionTint",
"displayName": " Transmission Tint",
"description": "Color of the volume light travelling through",
"description": "Color of the volume light traveling through",
"type": "Color",
"defaultValue": [ 1.0, 0.8, 0.6 ]
},
@ -792,7 +792,7 @@
{
"id": "enableDetailMaskTexture",
"displayName": " Use Texture",
"description": "Enable detail mask texture",
"description": "Enable detail blend mask",
"type": "Bool",
"defaultValue": true
},
@ -811,7 +811,7 @@
{
"id": "textureMapUv",
"displayName": "Detail Map UVs",
"description": "Which UV set to use for detail map texture sampling",
"description": "Which UV set to use for detail map sampling",
"type": "Enum",
"enumIsUv": true,
"defaultValue": "Unwrapped",
@ -829,8 +829,8 @@
},
{
"id": "baseColorDetailMap",
"displayName": " Texture Map",
"description": "Detailed Base Color Texture map",
"displayName": " Texture",
"description": "Detailed Base Color Texture",
"type": "Image",
"connection": {
"type": "ShaderInput",
@ -853,7 +853,7 @@
{
"id": "enableNormals",
"displayName": "Enable Normal",
"description": "Enable detail normal texture to be used for fine detail normal such as scratches and small dents",
"description": "Enable detail normal map to be used for fine detail normal such as scratches and small dents",
"type": "Bool",
"defaultValue": false
},
@ -872,8 +872,8 @@
},
{
"id": "normalDetailMap",
"displayName": " Texture Map",
"description": "Detailed Normal Texture map",
"displayName": " Texture",
"description": "Detailed Normal map",
"type": "Image",
"connection": {
"type": "ShaderInput",
@ -1080,7 +1080,7 @@
{
"type": "Lua",
"args": {
"file": "StandardPBR_SubsurfaceState.lua"
"file": "EnhancedPBR_SubsurfaceState.lua"
}
},
{

@ -23,26 +23,11 @@
"displayName": "Specular Reflectance f0",
"description": "The constant f0 represents the specular reflectance at normal incidence (Fresnel 0 Angle). Used to adjust reflectance of non-metal surfaces."
},
{
"id": "clearCoat",
"displayName": "Clear Coat",
"description": "Properties for configuring gloss clear coat"
},
{
"id": "normal",
"displayName": "Normal",
"description": "Properties related to configuring surface normal."
},
{
"id": "opacity",
"displayName": "Opacity",
"description": "Properties for configuring the materials transparency."
},
{
"id": "uv",
"displayName": "UVs",
"description": "Properties for configuring UV transforms."
},
{
"id": "occlusion",
"displayName": "Occlusion",
@ -53,15 +38,25 @@
"displayName": "Emissive",
"description": "Properties to add light emission, independent of other lights in the scene."
},
{
"id": "clearCoat",
"displayName": "Clear Coat",
"description": "Properties for configuring gloss clear coat"
},
{
"id": "parallax",
"displayName": "Parallax Mapping",
"description": "Properties for parallax effect produced by depthmap."
"displayName": "Displacement",
"description": "Properties for parallax effect produced by a height map."
},
{
"id": "subsurfaceScattering",
"displayName": "Subsurface Scattering",
"description": "Properties for configuring subsurface scattering effects."
"id": "opacity",
"displayName": "Opacity",
"description": "Properties for configuring the materials transparency."
},
{
"id": "uv",
"displayName": "UVs",
"description": "Properties for configuring UV transforms."
},
{
// Note: this property group is used in the DiffuseGlobalIllumination pass, it is not read by the StandardPBR shader
@ -71,7 +66,7 @@
},
{
"id": "general",
"displayName": "General",
"displayName": "General Settings",
"description": "General settings."
}
],
@ -182,7 +177,7 @@
},
{
"id": "textureMap",
"displayName": "Texture Map",
"displayName": "Texture",
"description": "Base color texture map",
"type": "Image",
"connection": {
@ -193,14 +188,14 @@
{
"id": "useTexture",
"displayName": "Use Texture",
"description": "Whether to use the texture map.",
"description": "Whether to use the texture.",
"type": "Bool",
"defaultValue": true
},
{
"id": "textureMapUv",
"displayName": "UV",
"description": "Base color texture map UV set",
"description": "Base color map UV set",
"type": "Enum",
"enumIsUv": true,
"defaultValue": "Tiled",
@ -212,7 +207,7 @@
{
"id": "textureBlendMode",
"displayName": "Texture Blend Mode",
"description": "Selects the equation to use when combining Color, Factor, and Texture Map.",
"description": "Selects the equation to use when combining Color, Factor, and Texture.",
"type": "Enum",
"enumValues": [ "Multiply", "LinearLight", "Lerp", "Overlay" ],
"defaultValue": "Multiply",
@ -238,7 +233,7 @@
},
{
"id": "textureMap",
"displayName": "Texture Map",
"displayName": "Texture",
"description": "",
"type": "Image",
"connection": {
@ -249,14 +244,14 @@
{
"id": "useTexture",
"displayName": "Use Texture",
"description": "Whether to use the texture map, or just default to the Factor value.",
"description": "Whether to use the texture, or just default to the Factor value.",
"type": "Bool",
"defaultValue": true
},
{
"id": "textureMapUv",
"displayName": "UV",
"description": "Metallic texture map UV set",
"description": "Metallic map UV set",
"type": "Enum",
"enumIsUv": true,
"defaultValue": "Tiled",
@ -269,8 +264,8 @@
"roughness": [
{
"id": "textureMap",
"displayName": "Texture Map",
"description": "Texture map for defining surface roughness.",
"displayName": "Texture",
"description": "Texture for defining surface roughness.",
"type": "Image",
"connection": {
"type": "ShaderInput",
@ -280,14 +275,14 @@
{
"id": "useTexture",
"displayName": "Use Texture",
"description": "Whether to use the texture map, or just default to the Factor value.",
"description": "Whether to use the texture, or just default to the Factor value.",
"type": "Bool",
"defaultValue": true
},
{
"id": "textureMapUv",
"displayName": "UV",
"description": "Roughness texture map UV set",
"description": "Roughness map UV set",
"type": "Enum",
"enumIsUv": true,
"defaultValue": "Tiled",
@ -300,7 +295,7 @@
// Note that "factor" is mutually exclusive with "lowerBound"/"upperBound". These are swapped by a lua functor.
"id": "lowerBound",
"displayName": "Lower Bound",
"description": "The roughness value that corresponds to black in the texture map.",
"description": "The roughness value that corresponds to black in the texture.",
"type": "Float",
"defaultValue": 0.0,
"min": 0.0,
@ -314,7 +309,7 @@
// Note that "factor" is mutually exclusive with "lowerBound"/"upperBound". These are swapped by a lua functor.
"id": "upperBound",
"displayName": "Upper Bound",
"description": "The roughness value that corresponds to white in the texture map.",
"description": "The roughness value that corresponds to white in the texture.",
"type": "Float",
"defaultValue": 1.0,
"min": 0.0,
@ -355,8 +350,8 @@
},
{
"id": "textureMap",
"displayName": "Texture Map",
"description": "Texture map for defining surface reflectance.",
"displayName": "Texture",
"description": "Texture for defining surface reflectance.",
"type": "Image",
"connection": {
"type": "ShaderInput",
@ -366,14 +361,14 @@
{
"id": "useTexture",
"displayName": "Use Texture",
"description": "Whether to use the texture map, or just default to the Factor value.",
"description": "Whether to use the texture, or just default to the Factor value.",
"type": "Bool",
"defaultValue": true
},
{
"id": "textureMapUv",
"displayName": "UV",
"description": "Specular reflection texture map UV set",
"description": "Specular reflection map UV set",
"type": "Enum",
"enumIsUv": true,
"defaultValue": "Tiled",
@ -418,7 +413,7 @@
{
"id": "influenceMap",
"displayName": " Influence Map",
"description": "Strength factor texture map",
"description": "Strength factor texture",
"type": "Image",
"connection": {
"type": "ShaderInput",
@ -428,14 +423,14 @@
{
"id": "useInfluenceMap",
"displayName": " Use Texture",
"description": "Whether to use the texture map, or just default to the Factor value.",
"description": "Whether to use the texture, or just default to the Factor value.",
"type": "Bool",
"defaultValue": true
},
{
"id": "influenceMapUv",
"displayName": " UV",
"description": "Strength factor texture map UV set",
"description": "Strength factor map UV set",
"type": "Enum",
"enumIsUv": true,
"defaultValue": "Tiled",
@ -460,7 +455,7 @@
{
"id": "roughnessMap",
"displayName": " Roughness Map",
"description": "Roughness texture map",
"description": "Texture for defining surface roughness",
"type": "Image",
"connection": {
"type": "ShaderInput",
@ -470,14 +465,14 @@
{
"id": "useRoughnessMap",
"displayName": " Use Texture",
"description": "Whether to use the texture map, or just default to the roughness value.",
"description": "Whether to use the texture, or just default to the roughness value.",
"type": "Bool",
"defaultValue": true
},
{
"id": "roughnessMapUv",
"displayName": " UV",
"description": "Roughness texture map UV set",
"description": "Roughness map UV set",
"type": "Enum",
"enumIsUv": true,
"defaultValue": "Tiled",
@ -519,7 +514,7 @@
{
"id": "normalMapUv",
"displayName": " UV",
"description": "Normal texture map UV set",
"description": "Normal map UV set",
"type": "Enum",
"enumIsUv": true,
"defaultValue": "Tiled",
@ -532,8 +527,8 @@
"normal": [
{
"id": "textureMap",
"displayName": "Texture Map",
"description": "Texture map for defining surface normal direction.",
"displayName": "Texture",
"description": "Texture for defining surface normal direction.",
"type": "Image",
"connection": {
"type": "ShaderInput",
@ -543,14 +538,14 @@
{
"id": "useTexture",
"displayName": "Use Texture",
"description": "Whether to use the texture map, or just rely on vertex normals.",
"description": "Whether to use the texture, or just rely on vertex normals.",
"type": "Bool",
"defaultValue": true
},
{
"id": "textureMapUv",
"displayName": "UV",
"description": "Normal texture map UV set",
"description": "Normal map UV set",
"type": "Enum",
"enumIsUv": true,
"defaultValue": "Tiled",
@ -599,7 +594,7 @@
{
"id": "mode",
"displayName": "Opacity Mode",
"description": "Opacity mode for this texture.",
"description": "Indicates the general approach how transparency is to be applied.",
"type": "Enum",
"enumValues": [ "Opaque", "Cutout", "Blended", "TintedTransparent" ],
"defaultValue": "Opaque",
@ -611,7 +606,7 @@
{
"id": "alphaSource",
"displayName": "Alpha Source",
"description": "Source texture of alpha value.",
"description": "Indicates whether to get the opacity texture from the Base Color map (Packed) or from a separate greyscale texture (Split).",
"type": "Enum",
"enumValues": [ "Packed", "Split", "None" ],
"defaultValue": "Packed",
@ -622,8 +617,8 @@
},
{
"id": "textureMap",
"displayName": "Texture Map",
"description": "Texture map for defining surface opacity.",
"displayName": "Texture",
"description": "Texture for defining surface opacity.",
"type": "Image",
"connection": {
"type": "ShaderInput",
@ -633,7 +628,7 @@
{
"id": "textureMapUv",
"displayName": "UV",
"description": "Opacity texture map UV set",
"description": "Opacity map UV set",
"type": "Enum",
"enumIsUv": true,
"defaultValue": "Tiled",
@ -728,7 +723,7 @@
{
"id": "diffuseTextureMap",
"displayName": "Diffuse AO",
"description": "Texture map for defining occlusion area for diffuse ambient lighting.",
"description": "Texture for defining occlusion area for diffuse ambient lighting.",
"type": "Image",
"connection": {
"type": "ShaderInput",
@ -738,14 +733,14 @@
{
"id": "diffuseUseTexture",
"displayName": " Use Texture",
"description": "Whether to use the Diffuse AO texture map.",
"description": "Whether to use the Diffuse AO map.",
"type": "Bool",
"defaultValue": true
},
{
"id": "diffuseTextureMapUv",
"displayName": " UV",
"description": "Diffuse AO texture map UV set.",
"description": "Diffuse AO map UV set.",
"type": "Enum",
"enumIsUv": true,
"defaultValue": "Tiled",
@ -770,7 +765,7 @@
{
"id": "specularTextureMap",
"displayName": "Specular Cavity",
"description": "Texture map for defining occlusion area for specular lighting.",
"description": "Texture for defining occlusion area for specular lighting.",
"type": "Image",
"connection": {
"type": "ShaderInput",
@ -780,14 +775,14 @@
{
"id": "specularUseTexture",
"displayName": " Use Texture",
"description": "Whether to use the Specular Cavity texture map.",
"description": "Whether to use the Specular Cavity map.",
"type": "Bool",
"defaultValue": true
},
{
"id": "specularTextureMapUv",
"displayName": " UV",
"description": "Specular Cavity texture map UV set.",
"description": "Specular Cavity map UV set.",
"type": "Enum",
"enumIsUv": true,
"defaultValue": "Tiled",
@ -850,8 +845,8 @@
},
{
"id": "textureMap",
"displayName": "Texture Map",
"description": "Texture map for defining emissive area.",
"displayName": "Texture",
"description": "Texture for defining emissive area.",
"type": "Image",
"connection": {
"type": "ShaderInput",
@ -861,14 +856,14 @@
{
"id": "useTexture",
"displayName": "Use Texture",
"description": "Whether to use the texture map.",
"description": "Whether to use the texture.",
"type": "Bool",
"defaultValue": true
},
{
"id": "textureMapUv",
"displayName": "UV",
"description": "Emissive texture map UV set",
"description": "Emissive map UV set",
"type": "Enum",
"enumIsUv": true,
"defaultValue": "Tiled",
@ -881,8 +876,8 @@
"parallax": [
{
"id": "textureMap",
"displayName": "Heightmap",
"description": "Displacement heightmap to create parallax effect.",
"displayName": "Height Map",
"description": "Displacement height map to create parallax effect.",
"type": "Image",
"connection": {
"type": "ShaderInput",
@ -892,14 +887,14 @@
{
"id": "useTexture",
"displayName": "Use Texture",
"description": "Whether to use the heightmap.",
"description": "Whether to use the height map.",
"type": "Bool",
"defaultValue": true
},
{
"id": "textureMapUv",
"displayName": "UV",
"description": "Heightmap UV set",
"description": "Height map UV set",
"type": "Enum",
"enumIsUv": true,
"defaultValue": "Tiled",
@ -910,8 +905,8 @@
},
{
"id": "factor",
"displayName": "Heightmap Scale",
"description": "The total height of the heightmap in local model units.",
"displayName": "Height Map Scale",
"description": "The total height of the height map in local model units.",
"type": "Float",
"defaultValue": 0.05,
"min": 0.0,
@ -972,7 +967,7 @@
{
"id": "showClipping",
"displayName": "Show Clipping",
"description": "Highlight areas where the heightmap is clipped by the mesh surface.",
"description": "Highlight areas where the height map is clipped by the mesh surface.",
"type": "Bool",
"defaultValue": false,
"connection": {
@ -981,183 +976,6 @@
}
}
],
"subsurfaceScattering": [
{
"id": "enableSubsurfaceScattering",
"displayName": "Subsurface Scattering",
"description": "Enable subsurface scattering feature, this will disable metallic and parallax mapping property due to incompatibility",
"type": "Bool",
"defaultValue": false,
"connection": {
"type": "ShaderOption",
"id": "o_enableSubsurfaceScattering"
}
},
{
"id": "subsurfaceScatterFactor",
"displayName": " Factor",
"description": "Strength factor for scaling percentage of subsurface scattering effect applied",
"type": "float",
"defaultValue": 1.0,
"min": 0.0,
"max": 1.0,
"connection": {
"type": "ShaderInput",
"id": "m_subsurfaceScatteringFactor"
}
},
{
"id": "influenceMap",
"displayName": " Influence Map",
"description": "Use texture map to control the strength of subsurface scattering",
"type": "Image",
"connection": {
"type": "ShaderInput",
"id": "m_subsurfaceScatteringInfluenceMap"
}
},
{
"id": "useInfluenceMap",
"displayName": " Use Influence Map",
"description": "Whether to use the texture map as influence mask.",
"type": "Bool",
"defaultValue": true
},
{
"id": "influenceMapUv",
"displayName": " UV",
"description": "Influence map UV set",
"type": "Enum",
"enumIsUv": true,
"defaultValue": "Tiled",
"connection": {
"type": "ShaderInput",
"id": "m_subsurfaceScatteringInfluenceMapUvIndex"
}
},
{
"id": "scatterColor",
"displayName": " Scatter color",
"description": "Color of volume light traveled through",
"type": "Color",
"defaultValue": [ 1.0, 0.27, 0.13 ]
},
{
"id": "scatterDistance",
"displayName": " Scatter distance",
"description": "How far light traveled inside the volume",
"type": "float",
"defaultValue": 8,
"min": 0.0,
"softMax": 20.0
},
{
"id": "quality",
"displayName": " Quality",
"description": "How much percent of sample will be used for each pixel, more samples improve quality and reduce artifacts, especially when the scatter distance is relatively large, but slow down computation time, 1.0 = full set 200 samples per pixel",
"type": "float",
"defaultValue": 0.4,
"min": 0.2,
"max": 1.0,
"connection": {
"type": "ShaderInput",
"id": "m_subsurfaceScatteringQuality"
}
},
{
"id": "transmissionMode",
"displayName": "Transmission",
"description": "Algorithm used for calculating transmission",
"type": "Enum",
"enumValues": [ "None", "ThickObject", "ThinObject" ],
"defaultValue": "None",
"connection": {
"type": "ShaderOption",
"id": "o_transmission_mode"
}
},
{
"id": "thickness",
"displayName": " Thickness",
"description": "Normalized global thickness, the maxima between this value (multiplied by thickness map if enabled) and thickness from shadow map (if applicable) will be used as final thickness of pixel",
"type": "float",
"defaultValue": 0.5,
"min": 0.0,
"max": 1.0
},
{
"id": "thicknessMap",
"displayName": " Thickness Map",
"description": "Use a greyscale texture for per pixel thickness",
"type": "Image",
"connection": {
"type": "ShaderInput",
"id": "m_transmissionThicknessMap"
}
},
{
"id": "useThicknessMap",
"displayName": " Use Thickness Map",
"description": "Whether to use the thickness map",
"type": "Bool",
"defaultValue": true
},
{
"id": "thicknessMapUv",
"displayName": " UV",
"description": "Thickness map UV set",
"type": "Enum",
"enumIsUv": true,
"defaultValue": "Tiled",
"connection": {
"type": "ShaderInput",
"id": "m_transmissionThicknessMapUvIndex"
}
},
{
"id": "transmissionTint",
"displayName": " Transmission Tint",
"description": "Color of the volume light travelling through",
"type": "Color",
"defaultValue": [ 1.0, 0.8, 0.6 ]
},
{
"id": "transmissionPower",
"displayName": " Power",
"description": "How much transmitted light scatter radially ",
"type": "float",
"defaultValue": 6.0,
"min": 0.0,
"softMax": 20.0
},
{
"id": "transmissionDistortion",
"displayName": " Distortion",
"description": "How much light direction distorted towards surface normal",
"type": "float",
"defaultValue": 0.1,
"min": 0.0,
"max": 1.0
},
{
"id": "transmissionAttenuation",
"displayName": " Attenuation",
"description": "How fast transmitted light fade with thickness",
"type": "float",
"defaultValue": 4.0,
"min": 0.0,
"softMax": 20.0
},
{
"id": "transmissionScale",
"displayName": " Scale",
"description": "Strength of transmission",
"type": "float",
"defaultValue": 3.0,
"min": 0.0,
"softMax": 20.0
}
],
"irradiance": [
// Note: this property group is used in the DiffuseGlobalIllumination pass and not by the main forward shader
{
@ -1261,25 +1079,6 @@
"nitMinMax": [0.001, 100000.0]
}
},
{
// Preprocess & build parameter set for subsurface scattering and translucency
"type": "HandleSubsurfaceScatteringParameters",
"args": {
"mode": "subsurfaceScattering.transmissionMode",
"scale": "subsurfaceScattering.transmissionScale",
"power": "subsurfaceScattering.transmissionPower",
"distortion": "subsurfaceScattering.transmissionDistortion",
"attenuation": "subsurfaceScattering.transmissionAttenuation",
"tintColor": "subsurfaceScattering.transmissionTint",
"thickness": "subsurfaceScattering.thickness",
"enabled": "subsurfaceScattering.enableSubsurfaceScattering",
"scatterDistanceColor": "subsurfaceScattering.scatterColor",
"scatterDistanceIntensity": "subsurfaceScattering.scatterDistance",
"scatterDistanceShaderInput": "m_scatterDistance",
"parametersShaderInput": "m_transmissionParams",
"tintThickenssShaderInput": "m_transmissionTintThickness"
}
},
{
"type": "UseTexture",
"args": {
@ -1364,12 +1163,6 @@
"file": "StandardPBR_Roughness.lua"
}
},
{
"type": "Lua",
"args": {
"file": "StandardPBR_SubsurfaceState.lua"
}
},
{
"type": "Lua",
"args": {

@ -73,25 +73,6 @@ ShaderResourceGroup MaterialSrg : SRG_PerMaterial
MagFilter = Linear;
MipFilter = Linear;
};
// Parameters for subsurface scattering
float m_subsurfaceScatteringFactor;
float m_subsurfaceScatteringQuality;
float3 m_scatterDistance;
Texture2D m_subsurfaceScatteringInfluenceMap;
uint m_subsurfaceScatteringInfluenceMapUvIndex;
// Parameters for transmission
// Elements of m_transmissionParams:
// Thick object mode: (attenuation coefficient, power, distortion, scale)
// Thin object mode: (float3 scatter distance, scale)
float4 m_transmissionParams;
// (float3 TintColor, thickness)
float4 m_transmissionTintThickness;
Texture2D m_transmissionThicknessMap;
uint m_transmissionThicknessMapUvIndex;
}
// Callback function for ParallaxMapping.azsli

@ -47,13 +47,6 @@ COMMON_OPTIONS_EMISSIVE()
// Alpha
#include "MaterialInputs/AlphaInput.azsli"
// Subsurface
#include "MaterialInputs/SubsurfaceInput.azsli"
// Transmission
#include "MaterialInputs/TransmissionInput.azsli"
// ---------- Vertex Shader ----------
struct VSInput
@ -113,7 +106,7 @@ PbrLightingOutput ForwardPassPS_Common(VSOutput IN, bool isFrontFace, out float
float3 tangents[UvSetCount] = { IN.m_tangent.xyz, IN.m_tangent.xyz };
float3 bitangents[UvSetCount] = { IN.m_bitangent.xyz, IN.m_bitangent.xyz };
if ((o_parallax_feature_enabled && !o_enableSubsurfaceScattering) || o_normal_useTexture || (o_clearCoat_enabled && o_clearCoat_normal_useTexture))
if (ShouldHandleParallax() || o_normal_useTexture || (o_clearCoat_enabled && o_clearCoat_normal_useTexture))
{
PrepareGeneratedTangent(IN.m_normal, IN.m_worldPosition, isFrontFace, IN.m_uv, UvSetCount, tangents, bitangents);
}
@ -124,7 +117,6 @@ PbrLightingOutput ForwardPassPS_Common(VSOutput IN, bool isFrontFace, out float
bool displacementIsClipped = false;
// Parallax mapping's non uniform uv transformations break screen space subsurface scattering, disable it when subsurface scatteirng is enabled
if(ShouldHandleParallax())
{
@ -174,12 +166,8 @@ PbrLightingOutput ForwardPassPS_Common(VSOutput IN, bool isFrontFace, out float
// ------- Metallic -------
float metallic = 0;
if(!o_enableSubsurfaceScattering) // If subsurface scattering is enabled skip texture lookup for metallic, as this quantity won't be used anyway
{
float2 metallicUv = IN.m_uv[MaterialSrg::m_metallicMapUvIndex];
metallic = GetMetallicInput(MaterialSrg::m_metallicMap, MaterialSrg::m_sampler, metallicUv, MaterialSrg::m_metallicFactor, o_metallic_useTexture);
}
float2 metallicUv = IN.m_uv[MaterialSrg::m_metallicMapUvIndex];
float metallic = GetMetallicInput(MaterialSrg::m_metallicMap, MaterialSrg::m_sampler, metallicUv, MaterialSrg::m_metallicFactor, o_metallic_useTexture);
// ------- Specular -------
@ -195,11 +183,6 @@ PbrLightingOutput ForwardPassPS_Common(VSOutput IN, bool isFrontFace, out float
MaterialSrg::m_roughnessLowerBound, MaterialSrg::m_roughnessUpperBound, o_roughness_useTexture);
surface.CalculateRoughnessA();
// ------- Subsurface -------
float surfaceScatteringFactor = 0.0f;
surface.transmission.InitializeToZero();
// ------- Lighting Data -------
LightingData lightingData;
@ -271,7 +254,7 @@ PbrLightingOutput ForwardPassPS_Common(VSOutput IN, bool isFrontFace, out float
ApplyIBL(surface, lightingData);
// Finalize Lighting
lightingData.FinalizeLighting(surface.transmission.tint);
lightingData.FinalizeLighting();
if (o_opacity_mode == OpacityMode::Blended || o_opacity_mode == OpacityMode::TintedTransparent)
@ -312,13 +295,6 @@ PbrLightingOutput ForwardPassPS_Common(VSOutput IN, bool isFrontFace, out float
lightingOutput.m_diffuseColor.rgb += lightingOutput.m_specularColor.rgb; // add specular
lightingOutput.m_specularColor.rgb = baseColor * (1.0 - lightingOutput.m_diffuseColor.w);
}
else
{
// Pack factor and quality, drawback: because of precision limit of float16 cannot represent exact 1, maximum representable value is 0.9961
uint factorAndQuality = dot(round(float2(saturate(surfaceScatteringFactor), MaterialSrg::m_subsurfaceScatteringQuality) * 255), float2(256, 1));
lightingOutput.m_diffuseColor.w = factorAndQuality * (o_enableSubsurfaceScattering ? 1.0 : -1.0);
lightingOutput.m_scatterDistance = MaterialSrg::m_scatterDistance;
}
return lightingOutput;
}

@ -54,6 +54,7 @@ class LightingData
void Init(float3 positionWS, float3 normal, float roughnessLinear);
void CalculateMultiscatterCompensation(float3 specularF0, bool enabled);
void FinalizeLighting();
void FinalizeLighting(float3 transmissionTint);
};
@ -80,10 +81,15 @@ void LightingData::CalculateMultiscatterCompensation(float3 specularF0, bool ena
multiScatterCompensation = GetMultiScatterCompensation(specularF0, brdf, enabled);
}
void LightingData::FinalizeLighting(float3 transmissionTint)
void LightingData::FinalizeLighting()
{
specularLighting *= specularOcclusion;
specularLighting += emissiveLighting;
}
void LightingData::FinalizeLighting(float3 transmissionTint)
{
FinalizeLighting();
// Transmitted light
if(o_transmission_mode != TransmissionMode::None)

@ -75,7 +75,6 @@ struct PbrLightingOutput
float4 m_albedo;
float4 m_specularF0;
float4 m_normal;
float3 m_scatterDistance;
};

@ -20,7 +20,7 @@
class Surface
{
ClearCoatSurfaceData clearCoat;
TransmissionSurfaceData transmission;
TransmissionSurfaceData transmission; // This is not actually used for Standard PBR, but must be present for common lighting code to compile
// ------- BasePbrSurfaceData -------

@ -0,0 +1,48 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include <Atom/Features/SrgSemantics.azsli>
ShaderResourceGroup RayTracingMaterialSrg : SRG_RayTracingMaterial
{
Sampler LinearSampler
{
AddressU = Wrap;
AddressV = Wrap;
MinFilter = Linear;
MagFilter = Linear;
MipFilter = Linear;
MaxAnisotropy = 16;
};
// material info structured buffer
struct MaterialInfo
{
float4 m_baseColor;
float m_metallicFactor;
float m_roughnessFactor;
uint m_textureFlags;
uint m_textureStartIndex;
};
// hit shaders can retrieve the MaterialInfo for a mesh hit using: RayTracingMaterialSrg::m_materialInfo[InstanceIndex()]
StructuredBuffer<MaterialInfo> m_materialInfo;
// texture flag bits indicating if optional textures are present
#define TEXTURE_FLAG_BASECOLOR 1
#define TEXTURE_FLAG_NORMAL 2
#define TEXTURE_FLAG_METALLIC 4
#define TEXTURE_FLAG_ROUGHNESS 8
// unbounded array of Material textures
Texture2D m_materialTextures[];
}

@ -0,0 +1,69 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
struct TextureData
{
float4 m_baseColor;
float3 m_normal;
float m_metallic;
float m_roughness;
};
TextureData GetHitTextureData(RayTracingMaterialSrg::MaterialInfo materialInfo, float2 uv)
{
TextureData textureData = (TextureData)0;
uint textureIndex = materialInfo.m_textureStartIndex;
// base color
if (materialInfo.m_textureFlags & TEXTURE_FLAG_BASECOLOR)
{
textureData.m_baseColor = RayTracingMaterialSrg::m_materialTextures[textureIndex++].SampleLevel(RayTracingMaterialSrg::LinearSampler, uv, 0);
}
else
{
textureData.m_baseColor = materialInfo.m_baseColor;
}
// normal
if (materialInfo.m_textureFlags & TEXTURE_FLAG_NORMAL)
{
textureData.m_normal = RayTracingMaterialSrg::m_materialTextures[textureIndex++].SampleLevel(RayTracingMaterialSrg::LinearSampler, uv, 0);
}
else
{
textureData.m_normal = float3(0.0f, 0.0f, 1.0f);
}
// metallic
if (materialInfo.m_textureFlags & TEXTURE_FLAG_METALLIC)
{
textureData.m_metallic = RayTracingMaterialSrg::m_materialTextures[textureIndex++].SampleLevel(RayTracingMaterialSrg::LinearSampler, uv, 0);
}
else
{
textureData.m_metallic = materialInfo.m_metallicFactor;
}
// roughness
if (materialInfo.m_textureFlags & TEXTURE_FLAG_ROUGHNESS)
{
textureData.m_roughness = RayTracingMaterialSrg::m_materialTextures[textureIndex++].SampleLevel(RayTracingMaterialSrg::LinearSampler, uv, 0);
}
else
{
textureData.m_roughness = materialInfo.m_roughnessFactor;
}
return textureData;
}

@ -136,18 +136,35 @@ ShaderResourceGroup RayTracingSceneSrg : SRG_RayTracingScene
uint m_indexOffset;
uint m_positionOffset;
uint m_normalOffset;
uint m_tangentOffset;
uint m_bitangentOffset;
uint m_uvOffset;
float m_padding0[2];
float4 m_irradianceColor;
float3x3 m_worldInvTranspose;
float m_padding1;
uint m_bufferFlags;
uint m_bufferStartIndex;
};
// hit shaders can retrieve the MeshInfo for a mesh hit using: RayTracingSceneSrg::m_meshInfo[InstanceIndex()]
StructuredBuffer<MeshInfo> m_meshInfo;
// unbounded array of Index, VertexPosition, and VertexNormal buffers
// each mesh has three entries in this array starting at its InstanceIndex() * BUFFER_COUNT_PER_MESH
#define BUFFER_COUNT_PER_MESH 3
#define MESH_INDEX_BUFFER_OFFSET 0
#define MESH_POSITION_BUFFER_OFFSET 1
#define MESH_NORMAL_BUFFER_OFFSET 2
// buffer array index offsets for buffers that are always present for each mesh
#define MESH_INDEX_BUFFER_OFFSET 0
#define MESH_POSITION_BUFFER_OFFSET 1
#define MESH_NORMAL_BUFFER_OFFSET 2
#define MESH_TANGENT_BUFFER_OFFSET 3
#define MESH_BITANGENT_BUFFER_OFFSET 4
// buffer flag bits indicating if optional buffers are present
#define MESH_BUFFER_FLAG_UV 1
// Unbounded array of mesh stream buffers:
// - Index, Position, Normal, Tangent, and Bitangent stream buffers are always present
// - Optional stream buffers such as UV are indicated in the MeshInfo.m_bufferFlags field
// - Buffers for a particular mesh start at MeshInfo.m_bufferStartIndex
ByteAddressBuffer m_meshBuffers[];
}

@ -0,0 +1,126 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
// returns the normalized camera view ray into the scene for this raytracing dispatch thread
float3 GetViewRayDirection(float4x4 viewProjectionInverseMatrix)
{
float2 pixel = ((float2)DispatchRaysIndex().xy + float2(0.5f, 0.5f)) / (float2)DispatchRaysDimensions();
float2 ndc = pixel * float2(2.0f, -2.0f) + float2(-1.0f, 1.0f);
return normalize(mul(viewProjectionInverseMatrix, float4(ndc, 0.0f, 1.0f)).xyz);
}
// returns the vertex indices for the primitive hit by the ray
// Note: usable only in a raytracing Hit shader
uint3 GetHitIndices(RayTracingSceneSrg::MeshInfo meshInfo)
{
// compute the array index of the index buffer for this mesh in the m_meshBuffers unbounded array
uint meshIndexBufferArrayIndex = meshInfo.m_bufferStartIndex + MESH_INDEX_BUFFER_OFFSET;
// compute the offset into the index buffer for this primitve of the mesh
uint offsetBytes = meshInfo.m_indexOffset + (PrimitiveIndex() * 12);
// load the indices for this primitive from the index buffer
return RayTracingSceneSrg::m_meshBuffers[meshIndexBufferArrayIndex].Load3(offsetBytes);
}
// returns the interpolated vertex data for the primitive hit by the ray
// Note: usable only in a raytracing hit shader
struct VertexData
{
float3 m_position;
float3 m_normal;
float3 m_tangent;
float3 m_bitangent;
float2 m_uv;
};
VertexData GetHitInterpolatedVertexData(RayTracingSceneSrg::MeshInfo meshInfo, float2 builtInBarycentrics)
{
// retrieve the poly indices
uint3 indices = GetHitIndices(meshInfo);
// compute barycentrics
float3 barycentrics = float3((1.0f - builtInBarycentrics.x - builtInBarycentrics.y), builtInBarycentrics.x, builtInBarycentrics.y);
// compute the vertex data using barycentric interpolation
VertexData vertexData = (VertexData)0;
for (uint i = 0; i < 3; ++i)
{
// position
{
// array index of the position buffer for this mesh in the m_meshBuffers unbounded array
uint meshVertexPositionArrayIndex = meshInfo.m_bufferStartIndex + MESH_POSITION_BUFFER_OFFSET;
// offset into the position buffer for this vertex
uint positionOffset = meshInfo.m_positionOffset + (indices[i] * 12);
// load the position data
vertexData.m_position += asfloat(RayTracingSceneSrg::m_meshBuffers[meshVertexPositionArrayIndex].Load3(positionOffset)) * barycentrics[i];
}
// normal
{
// array index of the normal buffer for this mesh in the m_meshBuffers unbounded array
uint meshVertexNormalArrayIndex = meshInfo.m_bufferStartIndex + MESH_NORMAL_BUFFER_OFFSET;
// offset into the normal buffer for this vertex
uint normalOffset = meshInfo.m_normalOffset + (indices[i] * 12);
// load the normal data
vertexData.m_normal += asfloat(RayTracingSceneSrg::m_meshBuffers[meshVertexNormalArrayIndex].Load3(normalOffset)) * barycentrics[i];
}
// tangent
{
// array index of the tangent buffer for this mesh in the m_meshBuffers unbounded array
uint meshVertexTangentArrayIndex = meshInfo.m_bufferStartIndex + MESH_TANGENT_BUFFER_OFFSET;
// offset into the tangent buffer for this vertex
uint tangentOffset = meshInfo.m_tangentOffset + (indices[i] * 12);
// load the tangent data
vertexData.m_tangent += asfloat(RayTracingSceneSrg::m_meshBuffers[meshVertexTangentArrayIndex].Load3(tangentOffset)) * barycentrics[i];
}
// bitangent
{
// array index of the bitangent buffer for this mesh in the m_meshBuffers unbounded array
uint meshVertexBitangentArrayIndex = meshInfo.m_bufferStartIndex + MESH_BITANGENT_BUFFER_OFFSET;
// offset into the bitangent buffer for this vertex
uint bitangentOffset = meshInfo.m_bitangentOffset + (indices[i] * 12);
// load the bitangent data
vertexData.m_bitangent += asfloat(RayTracingSceneSrg::m_meshBuffers[meshVertexBitangentArrayIndex].Load3(bitangentOffset)) * barycentrics[i];
}
// optional streams begin after MESH_BITANGENT_BUFFER_OFFSET
uint optionalBufferOffset = MESH_BITANGENT_BUFFER_OFFSET + 1;
// UV
if (meshInfo.m_bufferFlags & MESH_BUFFER_FLAG_UV)
{
// array index of the UV buffer for this mesh in the m_meshBuffers unbounded array
uint meshVertexUVArrayIndex = meshInfo.m_bufferStartIndex + optionalBufferOffset++;
// offset into the UV buffer for this vertex
uint uvOffset = meshInfo.m_uvOffset + (indices[i] * 8);
// load the UV data
vertexData.m_uv += asfloat(RayTracingSceneSrg::m_meshBuffers[meshVertexUVArrayIndex].Load2(uvOffset)) * barycentrics[i];
}
}
vertexData.m_normal = normalize(vertexData.m_normal);
return vertexData;
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save