Merge branch 'main' of https://github.com/aws-lumberyard/o3de into Prefab/Create/PositionFix
This commit is contained in:
@@ -115,7 +115,6 @@
|
||||
*.wav filter=lfs diff=lfs merge=lfs -text
|
||||
*.webm filter=lfs diff=lfs merge=lfs -text
|
||||
*.wem filter=lfs diff=lfs merge=lfs -text
|
||||
*.wxs filter=lfs diff=lfs merge=lfs -text
|
||||
*.zip filter=lfs diff=lfs merge=lfs -text
|
||||
*.tbscene filter=lfs diff=lfs merge=lfs -text
|
||||
*.spp filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"AWSResourceMappings": {},
|
||||
"AccountId": "",
|
||||
"Region": "us-west-2",
|
||||
"Version": "1.0.0"
|
||||
}
|
||||
@@ -45,4 +45,7 @@ set(GEM_DEPENDENCIES
|
||||
Gem::Atom_AtomBridge
|
||||
Gem::NvCloth
|
||||
Gem::Blast
|
||||
Gem::AWSCore
|
||||
Gem::AWSClientAuth
|
||||
Gem::AWSMetrics
|
||||
)
|
||||
|
||||
@@ -55,4 +55,7 @@ set(GEM_DEPENDENCIES
|
||||
Gem::Atom_AtomBridge.Editor
|
||||
Gem::NvCloth.Editor
|
||||
Gem::Blast.Editor
|
||||
Gem::AWSCore.Editor
|
||||
Gem::AWSClientAuth
|
||||
Gem::AWSMetrics
|
||||
)
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
################################################################################
|
||||
# AWS Automated Tests
|
||||
# Runs AWS Gems automation tests.
|
||||
################################################################################
|
||||
|
||||
if(PAL_TRAIT_BUILD_TESTS_SUPPORTED AND PAL_TRAIT_BUILD_HOST_TOOLS)
|
||||
# Enable after installing NodeJS and CDK on jenkins Windows AMI.
|
||||
#ly_add_pytest(
|
||||
# NAME AutomatedTesting::AWSTests
|
||||
# TEST_SUITE periodic
|
||||
# TEST_SERIAL
|
||||
# PATH ${CMAKE_CURRENT_LIST_DIR}/AWS/${PAL_PLATFORM_NAME}/
|
||||
# RUNTIME_DEPENDENCIES
|
||||
# Legacy::Editor
|
||||
# AZ::AssetProcessor
|
||||
# AutomatedTesting.Assets
|
||||
# COMPONENT
|
||||
# AWS
|
||||
#)
|
||||
endif()
|
||||
@@ -0,0 +1,155 @@
|
||||
"""
|
||||
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 os
|
||||
import pytest
|
||||
import boto3
|
||||
|
||||
import ly_test_tools.environment.process_utils as process_utils
|
||||
from typing import List
|
||||
|
||||
|
||||
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):
|
||||
"""
|
||||
:param cdk_path: Path where cdk app.py is stored.
|
||||
:param project: Project name used for cdk project name env variable.
|
||||
:param account_id: AWS account id to use with cdk application.
|
||||
:param workspace: ly_test_tools workspace fixture.
|
||||
"""
|
||||
self._cdk_env = os.environ.copy()
|
||||
self._cdk_env['O3DE_AWS_PROJECT_NAME'] = project
|
||||
self._cdk_env['O3DE_AWS_DEPLOY_REGION'] = session.region_name
|
||||
self._cdk_env['O3DE_AWS_DEPLOY_ACCOUNT'] = account_id
|
||||
self._cdk_env['PATH'] = f'{workspace.paths.engine_root()}\\python;' + self._cdk_env['PATH']
|
||||
|
||||
credentials = session.get_credentials().get_frozen_credentials()
|
||||
self._cdk_env['AWS_ACCESS_KEY_ID'] = credentials.access_key
|
||||
self._cdk_env['AWS_SECRET_ACCESS_KEY'] = credentials.secret_key
|
||||
self._cdk_env['AWS_SESSION_TOKEN'] = credentials.token
|
||||
self._stacks = []
|
||||
self._cdk_path = cdk_path
|
||||
|
||||
output = process_utils.check_output(
|
||||
'python -m pip install -r requirements.txt',
|
||||
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 []
|
||||
|
||||
list_cdk_application_cmd = ['cdk', 'list']
|
||||
output = process_utils.check_output(
|
||||
list_cdk_application_cmd,
|
||||
cwd=self._cdk_path,
|
||||
env=self._cdk_env,
|
||||
shell=True)
|
||||
|
||||
return output.splitlines()
|
||||
|
||||
def synthesize(self) -> None:
|
||||
"""
|
||||
Synthesizes all cdk stacks
|
||||
"""
|
||||
if not self._cdk_path:
|
||||
return
|
||||
|
||||
list_cdk_application_cmd = ['cdk', 'synth']
|
||||
|
||||
process_utils.check_output(
|
||||
list_cdk_application_cmd,
|
||||
cwd=self._cdk_path,
|
||||
env=self._cdk_env,
|
||||
shell=True)
|
||||
|
||||
def deploy(self, context_variable: str = '') -> List[str]:
|
||||
"""
|
||||
Deploys all the CDK stacks.
|
||||
:param context_variable: Context variable for enabling optional features.
|
||||
:return List of deployed stack arns.
|
||||
"""
|
||||
if not self._cdk_path:
|
||||
return []
|
||||
|
||||
deploy_cdk_application_cmd = ['cdk', 'deploy', '--require-approval', 'never']
|
||||
if context_variable:
|
||||
deploy_cdk_application_cmd.extend(['-c', f'{context_variable}'])
|
||||
|
||||
output = process_utils.check_output(
|
||||
deploy_cdk_application_cmd,
|
||||
cwd=self._cdk_path,
|
||||
env=self._cdk_env,
|
||||
shell=True)
|
||||
|
||||
stacks = []
|
||||
for line in output.splitlines():
|
||||
line_sections = line.split('/')
|
||||
assert len(line_sections), 3
|
||||
stacks.append(line.split('/')[-2])
|
||||
|
||||
return stacks
|
||||
|
||||
def destroy(self) -> None:
|
||||
"""
|
||||
Destroys the cdk application.
|
||||
"""
|
||||
destroy_cdk_application_cmd = ['cdk', 'destroy', '-f']
|
||||
process_utils.check_output(
|
||||
destroy_cdk_application_cmd,
|
||||
cwd=self._cdk_path,
|
||||
env=self._cdk_env,
|
||||
shell=True)
|
||||
|
||||
self._stacks = []
|
||||
self._cdk_path = ''
|
||||
|
||||
|
||||
@pytest.fixture(scope='function')
|
||||
def cdk(
|
||||
request: pytest.fixture,
|
||||
project: str,
|
||||
feature_name: str,
|
||||
workspace: pytest.fixture,
|
||||
aws_utils: pytest.fixture,
|
||||
destroy_stacks_on_teardown: bool = True) -> Cdk:
|
||||
"""
|
||||
Fixture for setting up a Cdk
|
||||
:param request: _pytest.fixtures.SubRequest class that handles getting
|
||||
a pytest fixture from a pytest function/fixture.
|
||||
:param project: Project name used for cdk project name env variable.
|
||||
: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 destroy_stacks_on_teardown: option to control calling destroy ot the end of test.
|
||||
:return Cdk class object.
|
||||
"""
|
||||
|
||||
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())
|
||||
|
||||
def teardown():
|
||||
if destroy_stacks_on_teardown:
|
||||
cdk_obj.destroy()
|
||||
request.addfinalizer(teardown)
|
||||
|
||||
return cdk_obj
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
"""
|
||||
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 pytest
|
||||
import os
|
||||
import logging
|
||||
import ly_test_tools.log.log_monitor
|
||||
|
||||
from AWS.Windows.resource_mappings.resource_mappings import resource_mappings
|
||||
from AWS.Windows.cdk.cdk import cdk
|
||||
from AWS.common.aws_utils import aws_utils
|
||||
from assetpipeline.ap_fixtures.asset_processor_fixture import asset_processor as asset_processor
|
||||
|
||||
AWS_PROJECT_NAME = 'AWS-AutomationTest'
|
||||
AWS_CLIENT_AUTH_FEATURE_NAME = 'AWSClientAuth'
|
||||
AWS_CLIENT_AUTH_DEFAULT_PROFILE_NAME = 'default'
|
||||
|
||||
GAME_LOG_NAME = 'Game.log'
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@pytest.mark.SUITE_periodic
|
||||
@pytest.mark.usefixtures('automatic_process_killer')
|
||||
@pytest.mark.usefixtures('asset_processor')
|
||||
@pytest.mark.usefixtures('workspace')
|
||||
@pytest.mark.parametrize('project', ['AutomatedTesting'])
|
||||
@pytest.mark.parametrize('level', ['AWS/ClientAuth'])
|
||||
@pytest.mark.usefixtures('cdk')
|
||||
@pytest.mark.parametrize('feature_name', [AWS_CLIENT_AUTH_FEATURE_NAME])
|
||||
@pytest.mark.usefixtures('resource_mappings')
|
||||
@pytest.mark.parametrize('resource_mappings_filename', ['aws_resource_mappings.json'])
|
||||
@pytest.mark.usefixtures('aws_utils')
|
||||
@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 TestAWSClientAuthAnonymousCredentials(object):
|
||||
"""
|
||||
Test class to verify AWS Cognito Identity pool anonymous authorization.
|
||||
"""
|
||||
|
||||
def test_anonymous_credentials(self,
|
||||
level: str,
|
||||
launcher: pytest.fixture,
|
||||
cdk: pytest.fixture,
|
||||
resource_mappings: pytest.fixture,
|
||||
workspace: pytest.fixture,
|
||||
asset_processor: pytest.fixture
|
||||
):
|
||||
"""
|
||||
Setup: Deploys cdk and updates resource mapping file.
|
||||
Tests: Getting AWS credentials for no signed in user.
|
||||
Verification: Log monitor looks for success credentials log.
|
||||
"""
|
||||
logger.info(f'Cdk stack names:\n{cdk.list()}')
|
||||
stacks = cdk.deploy()
|
||||
resource_mappings.populate_output_keys(stacks)
|
||||
asset_processor.start()
|
||||
asset_processor.wait_for_idle()
|
||||
|
||||
file_to_monitor = os.path.join(launcher.workspace.paths.project_log(), GAME_LOG_NAME)
|
||||
log_monitor = ly_test_tools.log.log_monitor.LogMonitor(launcher=launcher, log_file_path=file_to_monitor)
|
||||
|
||||
launcher.args = ['+LoadLevel', level]
|
||||
|
||||
with launcher.start(launch_ap=False):
|
||||
result = log_monitor.monitor_log_for_lines(
|
||||
expected_lines=['(Script) - Success anonymous credentials'],
|
||||
unexpected_lines=['(Script) - Fail anonymous credentials'],
|
||||
halt_on_unexpected=True,
|
||||
)
|
||||
assert result, 'Anonymous credentials fetched successfully.'
|
||||
@@ -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,137 @@
|
||||
"""
|
||||
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 os
|
||||
import pytest
|
||||
import json
|
||||
|
||||
AWS_RESOURCE_MAPPINGS_KEY = 'AWSResourceMappings'
|
||||
AWS_RESOURCE_MAPPINGS_ACCOUNT_ID_KEY = 'AccountId'
|
||||
AWS_RESOURCE_MAPPINGS_REGION_KEY = 'Region'
|
||||
|
||||
|
||||
class ResourceMappings:
|
||||
"""
|
||||
ResourceMappings class that handles writing Cloud formation outputs to resource mappings json file in a project.
|
||||
"""
|
||||
|
||||
def __init__(self, file_path: str, region: str, feature_name: str, account_id: str, workspace: pytest.fixture,
|
||||
cloud_formation_client):
|
||||
"""
|
||||
:param file_path: Path for the resource mapping file.
|
||||
:param region: Region value for the resource mapping file.
|
||||
:param feature_name: Feature gem name to use to append name to mappings key.
|
||||
:param account_id: AWS account id value for the resource mapping file.
|
||||
:param workspace: ly_test_tools workspace fixture.
|
||||
:param cloud_formation_client: AWS cloud formation client.
|
||||
"""
|
||||
self._cdk_env = os.environ.copy()
|
||||
self._cdk_env['PATH'] = f'{workspace.paths.engine_root()}\\python;' + self._cdk_env['PATH']
|
||||
self._resource_mapping_file_path = file_path
|
||||
self._region = region
|
||||
self._feature_name = feature_name
|
||||
self._account_id = account_id
|
||||
|
||||
assert os.path.exists(self._resource_mapping_file_path), \
|
||||
f'Invalid resource mapping file path {self._resource_mapping_file_path}'
|
||||
self._client = cloud_formation_client
|
||||
|
||||
def populate_output_keys(self, stacks=[]) -> None:
|
||||
"""
|
||||
Calls describe stacks on cloud formation service and persists outputs to resource mappings file.
|
||||
:param stacks List of stack arns to describe and populate resource mappings with.
|
||||
"""
|
||||
for stack_name in stacks:
|
||||
response = self._client.describe_stacks(
|
||||
StackName=stack_name
|
||||
)
|
||||
stacks = response.get('Stacks', [])
|
||||
assert len(stacks) == 1, f'{stack_name} is invalid.'
|
||||
|
||||
self.__write_resource_mappings(stacks[0].get('Outputs', []))
|
||||
|
||||
def __write_resource_mappings(self, outputs, append_feature_name = True) -> None:
|
||||
with open(self._resource_mapping_file_path) as file_content:
|
||||
resource_mappings = json.load(file_content)
|
||||
|
||||
resource_mappings[AWS_RESOURCE_MAPPINGS_ACCOUNT_ID_KEY] = self._account_id
|
||||
resource_mappings[AWS_RESOURCE_MAPPINGS_REGION_KEY] = self._region
|
||||
|
||||
# Append new mappings.
|
||||
resource_mappings[AWS_RESOURCE_MAPPINGS_KEY] = resource_mappings.get(AWS_RESOURCE_MAPPINGS_KEY, {})
|
||||
|
||||
for output in outputs:
|
||||
if append_feature_name:
|
||||
resource_key = f'{self._feature_name}.{output.get("OutputKey", "InvalidKey")}'
|
||||
else:
|
||||
resource_key = output.get("OutputKey", "InvalidKey")
|
||||
resource_mappings[AWS_RESOURCE_MAPPINGS_KEY][resource_key] = resource_mappings[
|
||||
AWS_RESOURCE_MAPPINGS_KEY].get(resource_key, {})
|
||||
resource_mappings[AWS_RESOURCE_MAPPINGS_KEY][resource_key]['Type'] = 'AutomationTestType'
|
||||
resource_mappings[AWS_RESOURCE_MAPPINGS_KEY][resource_key]['Name/ID'] = output.get('OutputValue',
|
||||
'InvalidId')
|
||||
|
||||
with open(self._resource_mapping_file_path, 'w') as file_content:
|
||||
json.dump(resource_mappings, file_content, indent=4)
|
||||
|
||||
def clear_output_keys(self) -> None:
|
||||
"""
|
||||
Clears values of all resource mapping keys. Sets region to default to us-west-2
|
||||
"""
|
||||
with open(self._resource_mapping_file_path) as file_content:
|
||||
resource_mappings = json.load(file_content)
|
||||
|
||||
resource_mappings[AWS_RESOURCE_MAPPINGS_ACCOUNT_ID_KEY] = ''
|
||||
resource_mappings[AWS_RESOURCE_MAPPINGS_REGION_KEY] = 'us-west-2'
|
||||
|
||||
# Append new mappings.
|
||||
resource_mappings[AWS_RESOURCE_MAPPINGS_KEY] = resource_mappings.get(AWS_RESOURCE_MAPPINGS_KEY, {})
|
||||
resource_mappings[AWS_RESOURCE_MAPPINGS_KEY] = {}
|
||||
|
||||
with open(self._resource_mapping_file_path, 'w') as file_content:
|
||||
json.dump(resource_mappings, file_content, indent=4)
|
||||
|
||||
self._resource_mapping_file_path = ''
|
||||
self._region = ''
|
||||
self._client = None
|
||||
|
||||
|
||||
@pytest.fixture(scope='function')
|
||||
def resource_mappings(
|
||||
request: pytest.fixture,
|
||||
project: str,
|
||||
feature_name: str,
|
||||
resource_mappings_filename: str,
|
||||
workspace: pytest.fixture,
|
||||
aws_utils: pytest.fixture) -> ResourceMappings:
|
||||
"""
|
||||
Fixture for setting up resource mappings file.
|
||||
:param request: _pytest.fixtures.SubRequest class that handles getting
|
||||
a pytest fixture from a pytest function/fixture.
|
||||
:param project: Project to find resource mapping file.
|
||||
:param feature_name: AWS Gem name that is prepended to resource mapping keys.
|
||||
:param resource_mappings_filename: Name of resource mapping file.
|
||||
:param workspace: ly_test_tools workspace fixture.
|
||||
:param aws_utils: AWS utils fixture.
|
||||
:return: ResourceMappings class object.
|
||||
"""
|
||||
|
||||
path = f'{workspace.paths.engine_root()}\\{project}\\Config\\{resource_mappings_filename}'
|
||||
resource_mappings_obj = ResourceMappings(path, aws_utils.assume_session().region_name, feature_name,
|
||||
aws_utils.assume_account_id(), workspace,
|
||||
aws_utils.client('cloudformation'))
|
||||
|
||||
def teardown():
|
||||
resource_mappings_obj.clear_output_keys()
|
||||
|
||||
request.addfinalizer(teardown)
|
||||
|
||||
return resource_mappings_obj
|
||||
@@ -0,0 +1,11 @@
|
||||
"""
|
||||
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,82 @@
|
||||
"""
|
||||
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
|
||||
@@ -56,5 +56,8 @@ add_subdirectory(editor)
|
||||
## Streaming ##
|
||||
add_subdirectory(streaming)
|
||||
|
||||
## Streaming ##
|
||||
## Smoke ##
|
||||
add_subdirectory(smoke)
|
||||
|
||||
## AWS ##
|
||||
add_subdirectory(AWS)
|
||||
|
||||
-5
@@ -98,11 +98,6 @@ class EditorTestHelper:
|
||||
self.test_success = False
|
||||
success = False
|
||||
|
||||
# Turn off any display info like FPS, as that will mess up our image comparisons
|
||||
# Turn off antialiasing as well
|
||||
general.run_console("r_displayInfo=0")
|
||||
general.run_console("r_antialiasingmode=0")
|
||||
general.idle_wait(1.0)
|
||||
return success
|
||||
|
||||
# Test Teardown
|
||||
|
||||
-1
@@ -18,7 +18,6 @@ from PySide2 import QtCore, QtWidgets, QtGui, QtTest
|
||||
from PySide2.QtWidgets import QAction, QWidget
|
||||
from PySide2.QtCore import Qt
|
||||
from PySide2.QtTest import QTest
|
||||
import azlmbr.legacy.general as general
|
||||
import traceback
|
||||
import threading
|
||||
import types
|
||||
|
||||
+13
-6
@@ -77,10 +77,10 @@ def run():
|
||||
|
||||
def verify_enter_exit_game_mode(component_name):
|
||||
general.enter_game_mode()
|
||||
TestHelper.wait_for_condition(lambda: general.is_in_game_mode(), 1.0)
|
||||
TestHelper.wait_for_condition(lambda: general.is_in_game_mode(), 2.0)
|
||||
general.log(f"{component_name}_test: Entered game mode: {general.is_in_game_mode()}")
|
||||
general.exit_game_mode()
|
||||
TestHelper.wait_for_condition(lambda: not general.is_in_game_mode(), 1.0)
|
||||
TestHelper.wait_for_condition(lambda: not general.is_in_game_mode(), 2.0)
|
||||
general.log(f"{component_name}_test: Exit game mode: {not general.is_in_game_mode()}")
|
||||
|
||||
def verify_hide_unhide_entity(component_name, entity_obj):
|
||||
@@ -97,16 +97,16 @@ def run():
|
||||
|
||||
def verify_deletion_undo_redo(component_name, entity_obj):
|
||||
editor.ToolsApplicationRequestBus(bus.Broadcast, "DeleteEntityById", entity_obj.id)
|
||||
TestHelper.wait_for_condition(lambda: not hydra.find_entity_by_name(entity_obj.name), 1.0)
|
||||
TestHelper.wait_for_condition(lambda: not hydra.find_entity_by_name(entity_obj.name), 2.0)
|
||||
general.log(f"{component_name}_test: Entity deleted: {not hydra.find_entity_by_name(entity_obj.name)}")
|
||||
|
||||
general.undo()
|
||||
TestHelper.wait_for_condition(lambda: hydra.find_entity_by_name(entity_obj.name) is not None, 1.0)
|
||||
TestHelper.wait_for_condition(lambda: hydra.find_entity_by_name(entity_obj.name) is not None, 2.0)
|
||||
general.log(f"{component_name}_test: UNDO entity deletion works: "
|
||||
f"{hydra.find_entity_by_name(entity_obj.name) is not None}")
|
||||
|
||||
general.redo()
|
||||
TestHelper.wait_for_condition(lambda: not hydra.find_entity_by_name(entity_obj.name), 1.0)
|
||||
TestHelper.wait_for_condition(lambda: not hydra.find_entity_by_name(entity_obj.name), 2.0)
|
||||
general.log(f"{component_name}_test: REDO entity deletion works: "
|
||||
f"{not hydra.find_entity_by_name(entity_obj.name)}")
|
||||
|
||||
@@ -120,7 +120,7 @@ def run():
|
||||
f"{not is_component_enabled(entity_obj.components[0])}")
|
||||
for component in components_to_add:
|
||||
entity_obj.add_component(component)
|
||||
TestHelper.wait_for_condition(lambda: is_component_enabled(entity_obj.components[0]), 1.0)
|
||||
TestHelper.wait_for_condition(lambda: is_component_enabled(entity_obj.components[0]), 2.0)
|
||||
general.log(
|
||||
f"{component_name}_test: Entity enabled after adding "
|
||||
f"required components: {is_component_enabled(entity_obj.components[0])}"
|
||||
@@ -135,7 +135,9 @@ def run():
|
||||
# Delete all existing entities initially
|
||||
search_filter = azlmbr.entity.SearchFilter()
|
||||
all_entities = entity.SearchBus(azlmbr.bus.Broadcast, "SearchEntities", search_filter)
|
||||
general.idle_wait_frames(1)
|
||||
editor.ToolsApplicationRequestBus(bus.Broadcast, "DeleteEntities", all_entities)
|
||||
general.idle_wait_frames(1)
|
||||
|
||||
class ComponentTests:
|
||||
"""Test launcher for each component."""
|
||||
@@ -147,9 +149,11 @@ def run():
|
||||
def run_component_tests(self):
|
||||
# Run common and additional tests
|
||||
entity_obj = create_entity_undo_redo_component_addition(self.component_name)
|
||||
general.idle_wait(0.5)
|
||||
|
||||
# Enter/Exit game mode test
|
||||
verify_enter_exit_game_mode(self.component_name)
|
||||
general.idle_wait(0.5)
|
||||
|
||||
# Any additional tests are executed here
|
||||
for test in self.additional_tests:
|
||||
@@ -157,13 +161,16 @@ def run():
|
||||
|
||||
# Hide/Unhide entity test
|
||||
verify_hide_unhide_entity(self.component_name, entity_obj)
|
||||
general.idle_wait(0.5)
|
||||
|
||||
# Deletion/Undo/Redo test
|
||||
verify_deletion_undo_redo(self.component_name, entity_obj)
|
||||
general.idle_wait(0.5)
|
||||
|
||||
# DepthOfField Component
|
||||
camera_entity = hydra.Entity("camera_entity")
|
||||
camera_entity.create_entity(math.Vector3(512.0, 512.0, 34.0), ["Camera"])
|
||||
general.idle_wait(0.5)
|
||||
depth_of_field = "DepthOfField"
|
||||
ComponentTests(
|
||||
depth_of_field,
|
||||
|
||||
@@ -163,7 +163,8 @@ class TestAtomEditorComponentsMain(object):
|
||||
]
|
||||
|
||||
unexpected_lines = [
|
||||
"failed to open",
|
||||
"Trace::Assert",
|
||||
"Trace::Error",
|
||||
"Traceback (most recent call last):",
|
||||
]
|
||||
|
||||
|
||||
@@ -64,8 +64,6 @@ class TestEditMenuOptions(EditorTestHelper):
|
||||
("Modify", "Transform Mode", "Move"),
|
||||
("Modify", "Transform Mode", "Rotate"),
|
||||
("Modify", "Transform Mode", "Scale"),
|
||||
("Lock Selection",),
|
||||
("Unlock All Entities",),
|
||||
("Editor Settings", "Global Preferences"),
|
||||
("Editor Settings", "Graphics Settings"),
|
||||
("Editor Settings", "Editor Settings Manager"),
|
||||
|
||||
@@ -50,7 +50,6 @@ class TestViewMenuOptions(EditorTestHelper):
|
||||
("Center on Selection",),
|
||||
("Show Quick Access Bar",),
|
||||
("Viewport", "Wireframe"),
|
||||
("Viewport", "Grid Settings"),
|
||||
("Viewport", "Go to Position"),
|
||||
("Viewport", "Center on Selection"),
|
||||
("Viewport", "Go to Location"),
|
||||
|
||||
@@ -40,7 +40,7 @@ class TestDocking(object):
|
||||
|
||||
@pytest.mark.test_case_id("C6376081")
|
||||
@pytest.mark.SUITE_periodic
|
||||
def test_basic_docked_tools(self, request, editor, level, launcher_platform):
|
||||
def test_Docking_BasicDockedTools(self, request, editor, level, launcher_platform):
|
||||
expected_lines = [
|
||||
"The tools are all docked together in a tabbed widget",
|
||||
"Entity Outliner works when docked, can select an Entity",
|
||||
|
||||
@@ -59,8 +59,6 @@ class TestMenus(object):
|
||||
"Move Action triggered",
|
||||
"Rotate Action triggered",
|
||||
"Scale Action triggered",
|
||||
"Lock Selection Action triggered",
|
||||
"Unlock All Entities Action triggered",
|
||||
"Global Preferences Action triggered",
|
||||
"Graphics Settings Action triggered",
|
||||
"Editor Settings Manager Action triggered",
|
||||
@@ -93,7 +91,6 @@ class TestMenus(object):
|
||||
"Center on Selection Action triggered",
|
||||
"Show Quick Access Bar Action triggered",
|
||||
"Wireframe Action triggered",
|
||||
"Grid Settings Action triggered",
|
||||
"Go to Position Action triggered",
|
||||
"Center on Selection Action triggered",
|
||||
"Go to Location Action triggered",
|
||||
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
|
||||
|
||||
class Tests:
|
||||
test_panes_visible = "All the test panes are opened"
|
||||
close_pane_1 = "Test pane 1 is closed"
|
||||
resize_pane_3 = "Test pane 3 resized successfully"
|
||||
location_changed = "Location of test pane 2 changed successfully"
|
||||
visiblity_retained = "Test pane retained its visiblity on Editor restart"
|
||||
location_retained = "Test pane retained its location on Editor restart"
|
||||
size_retained = "Test pane retained its size on Editor restart"
|
||||
|
||||
|
||||
def Pane_PropertiesChanged_RetainsOnRestart():
|
||||
"""
|
||||
Summary:
|
||||
The Script Canvas window is opened to verify if Script canvas panes can retain its visibility, size and location
|
||||
upon Editor restart.
|
||||
|
||||
Expected Behavior:
|
||||
The ScriptCanvas pane retain it's visiblity, size and location upon Editor restart.
|
||||
|
||||
Test Steps:
|
||||
1) Open Script Canvas window (Tools > Script Canvas)
|
||||
2) Make sure test panes are open and visible
|
||||
3) Close test pane 1
|
||||
4) Change dock location of test pane 2
|
||||
5) Resize test pane 3
|
||||
6) Restart Editor
|
||||
7) Verify if test pane 1 retain its visiblity
|
||||
8) Verify if location of test pane 2 is retained
|
||||
9) Verify if size of test pane 3 is retained
|
||||
10) Restore default layout and close SC window
|
||||
|
||||
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 sys
|
||||
|
||||
# Helper imports
|
||||
from utils import Report
|
||||
from utils import TestHelper as helper
|
||||
import pyside_utils
|
||||
|
||||
# Lumberyard Imports
|
||||
import azlmbr.legacy.general as general
|
||||
|
||||
# Pyside imports
|
||||
from PySide2 import QtCore, QtWidgets
|
||||
from PySide2.QtCore import Qt
|
||||
|
||||
# Constants
|
||||
TEST_CONDITION = sys.argv[1]
|
||||
TEST_PANE_1 = "NodePalette" # pane used to test visibility
|
||||
TEST_PANE_2 = "VariableManager" # pane used to test location
|
||||
TEST_PANE_3 = "NodeInspector" # pane used to test size
|
||||
SCALE_INT = 10 # Random resize scale integer
|
||||
DOCKAREA = Qt.TopDockWidgetArea # Preferred top area since no widget is docked on top
|
||||
|
||||
def click_menu_option(window, option_text):
|
||||
action = pyside_utils.find_child_by_pattern(window, {"text": option_text, "type": QtWidgets.QAction})
|
||||
action.trigger()
|
||||
|
||||
def find_pane(window, pane_name):
|
||||
return window.findChild(QtWidgets.QDockWidget, pane_name)
|
||||
|
||||
# Test starts here
|
||||
general.idle_enable(True)
|
||||
|
||||
# 1) Open Script Canvas window (Tools > Script Canvas)
|
||||
general.open_pane("Script Canvas")
|
||||
helper.wait_for_condition(lambda: general.is_pane_visible("Script Canvas"), 5.0)
|
||||
|
||||
if TEST_CONDITION == "before_restart":
|
||||
# 2) Make sure test panes are open and visible
|
||||
editor_window = pyside_utils.get_editor_main_window()
|
||||
sc = editor_window.findChild(QtWidgets.QDockWidget, "Script Canvas")
|
||||
click_menu_option(sc, "Restore Default Layout")
|
||||
test_pane_1 = sc.findChild(QtWidgets.QDockWidget, TEST_PANE_1)
|
||||
test_pane_2 = sc.findChild(QtWidgets.QDockWidget, TEST_PANE_2)
|
||||
test_pane_3 = sc.findChild(QtWidgets.QDockWidget, TEST_PANE_3)
|
||||
|
||||
result = test_pane_1.isVisible() and test_pane_2.isVisible() and test_pane_3.isVisible()
|
||||
Report.info(f"{Tests.test_panes_visible}: {result}")
|
||||
|
||||
# 3) Close test pane
|
||||
test_pane_1.close()
|
||||
Report.info(f"{Tests.close_pane_1}: {not test_pane_1.isVisible()}")
|
||||
|
||||
# 4) Change dock location of test pane 2
|
||||
sc_main = sc.findChild(QtWidgets.QMainWindow)
|
||||
sc_main.addDockWidget(DOCKAREA, find_pane(sc_main, TEST_PANE_2), QtCore.Qt.Vertical)
|
||||
Report.info(f"{Tests.location_changed}: {sc_main.dockWidgetArea(find_pane(sc_main, TEST_PANE_2)) == DOCKAREA}")
|
||||
|
||||
# 5) Resize test pane 3
|
||||
initial_size = test_pane_3.frameSize()
|
||||
test_pane_3.resize(initial_size.width() + SCALE_INT, initial_size.height() + SCALE_INT)
|
||||
new_size = test_pane_3.frameSize()
|
||||
resize_success = (
|
||||
abs(initial_size.width() - new_size.width()) == abs(initial_size.height() - new_size.height()) == SCALE_INT
|
||||
)
|
||||
Report.info(f"{Tests.resize_pane_3}: {resize_success}")
|
||||
|
||||
if TEST_CONDITION == "after_restart":
|
||||
try:
|
||||
# 6) Restart Editor
|
||||
# Restart is not possible through script and hence it is done by running the same file as 2 tests with a
|
||||
# condition as before_test and after_test
|
||||
|
||||
# 7) Verify if test pane 1 retain its visiblity
|
||||
# This pane closed before restart and expected that pane should not be visible.
|
||||
editor_window = pyside_utils.get_editor_main_window()
|
||||
sc = editor_window.findChild(QtWidgets.QDockWidget, "Script Canvas")
|
||||
Report.info(f"{Tests.visiblity_retained}: {not find_pane(sc, TEST_PANE_1).isVisible()}")
|
||||
|
||||
# 8) Verify if location of test pane 2 is retained
|
||||
# This pane was set at DOCKAREA lcoation before restart
|
||||
sc_main = sc.findChild(QtWidgets.QMainWindow)
|
||||
Report.info(
|
||||
f"{Tests.location_retained}: {sc_main.dockWidgetArea(find_pane(sc_main, TEST_PANE_2)) == DOCKAREA}"
|
||||
)
|
||||
|
||||
# 9) Verify if size of test pane 3 is retained
|
||||
# Verifying if size retained by checking current size not matching with default size
|
||||
test_pane_3 = find_pane(sc, TEST_PANE_3)
|
||||
retained_size = test_pane_3.frameSize()
|
||||
click_menu_option(sc, "Restore Default Layout")
|
||||
actual_size = test_pane_3.frameSize()
|
||||
Report.info(f"{Tests.size_retained}: {retained_size != actual_size}")
|
||||
|
||||
finally:
|
||||
# 10) Restore default layout and close SC window
|
||||
general.open_pane("Script Canvas")
|
||||
helper.wait_for_condition(lambda: general.is_pane_visible("Script Canvas"), 5.0)
|
||||
sc = editor_window.findChild(QtWidgets.QDockWidget, "Script Canvas")
|
||||
click_menu_option(sc, "Restore Default Layout")
|
||||
sc.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import ImportPathHelper as imports
|
||||
|
||||
imports.init()
|
||||
|
||||
from utils import Report
|
||||
|
||||
Report.start_test(Pane_PropertiesChanged_RetainsOnRestart)
|
||||
@@ -76,14 +76,8 @@ class TestAutomation(TestAutomationBase):
|
||||
from . import ScriptCanvasComponent_OnEntityActivatedDeactivated_PrintMessage as test_module
|
||||
self._run_test(request, workspace, editor, test_module)
|
||||
|
||||
<<<<<<< HEAD
|
||||
def test_NodePalette_HappyPath_ClearSelection(self, request, workspace, editor, launcher_platform, project):
|
||||
from . import NodePalette_HappyPath_ClearSelection as test_module
|
||||
=======
|
||||
@pytest.mark.test_case_id("T92562993")
|
||||
def test_NodePalette_ClearSelection(self, request, workspace, editor, launcher_platform, project):
|
||||
from . import NodePalette_ClearSelection as test_module
|
||||
>>>>>>> main
|
||||
self._run_test(request, workspace, editor, test_module)
|
||||
|
||||
@pytest.mark.parametrize("level", ["tmp_level"])
|
||||
@@ -119,7 +113,6 @@ class TestAutomation(TestAutomationBase):
|
||||
from . import Debugger_HappyPath_TargetMultipleGraphs as test_module
|
||||
self._run_test(request, workspace, editor, test_module)
|
||||
|
||||
@pytest.mark.test_case_id("T92569137")
|
||||
def test_Debugging_TargetMultipleGraphs(self, request, workspace, editor, launcher_platform, project):
|
||||
from . import Debugging_TargetMultipleGraphs as test_module
|
||||
self._run_test(request, workspace, editor, test_module)
|
||||
@@ -262,3 +255,37 @@ class TestScriptCanvasTests(object):
|
||||
auto_test_mode=False,
|
||||
timeout=60,
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"config",
|
||||
[
|
||||
{
|
||||
"cfg_args": "before_restart",
|
||||
"expected_lines": [
|
||||
"All the test panes are opened: True",
|
||||
"Test pane 1 is closed: True",
|
||||
"Location of test pane 2 changed successfully: True",
|
||||
"Test pane 3 resized successfully: True",
|
||||
],
|
||||
},
|
||||
{
|
||||
"cfg_args": "after_restart",
|
||||
"expected_lines": [
|
||||
"Test pane retained its visiblity on Editor restart: True",
|
||||
"Test pane retained its location on Editor restart: True",
|
||||
"Test pane retained its size on Editor restart: True",
|
||||
],
|
||||
},
|
||||
],
|
||||
)
|
||||
def test_Pane_PropertiesChanged_RetainsOnRestart(self, request, editor, config, project, launcher_platform):
|
||||
hydra.launch_and_validate_results(
|
||||
request,
|
||||
TEST_DIRECTORY,
|
||||
editor,
|
||||
"Pane_PropertiesChanged_RetainsOnRestart.py",
|
||||
config.get('expected_lines'),
|
||||
cfg_args=[config.get('cfg_args')],
|
||||
auto_test_mode=False,
|
||||
timeout=60,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f0f4d4e0155feaa76c80a14128000a0fd9570ab76e79f4847eaef9006324a4d2
|
||||
size 9084
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
<Environment />
|
||||
@@ -0,0 +1 @@
|
||||
<TimeOfDay Time="12"/>
|
||||
@@ -0,0 +1,6 @@
|
||||
<download name="ClientAuth" type="Map">
|
||||
<index src="filelist.xml" dest="filelist.xml"/>
|
||||
<files>
|
||||
<file src="level.pak" dest="level.pak" size="ED3" md5="3487525589271b5744ca83734fe3baa6"/>
|
||||
</files>
|
||||
</download>
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4900bdf28654e21032e69957f2762fa0a3b93a4b82163267a1f10f19f6d78692
|
||||
size 3795
|
||||
@@ -0,0 +1,12 @@
|
||||
0,0,0,0,0,0
|
||||
0,0,0,0,0,0
|
||||
0,0,0,0,0,0
|
||||
0,0,0,0,0,0
|
||||
0,0,0,0,0,0
|
||||
0,0,0,0,0,0
|
||||
0,0,0,0,0,0
|
||||
0,0,0,0,0,0
|
||||
0,0,0,0,0,0
|
||||
0,0,0,0,0,0
|
||||
0,0,0,0,0,0
|
||||
0,0,0,0,0,0
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"Amazon":
|
||||
{
|
||||
"AWSCore":
|
||||
{
|
||||
"ProfileName": "default",
|
||||
"ResourceMappingConfigFileName": "aws_resource_mappings.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,900 @@
|
||||
/*
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
// Original file Copyright Crytek GMBH or its affiliates, used under license.
|
||||
|
||||
#include "CrySystem_precompiled.h"
|
||||
#include "DebugCallStack.h"
|
||||
|
||||
#if defined(WIN32) || defined(WIN64)
|
||||
|
||||
#include <IConsole.h>
|
||||
#include <CryPath.h>
|
||||
#include "System.h"
|
||||
|
||||
#include <AzCore/Debug/StackTracer.h>
|
||||
#include <AzCore/Debug/EventTraceDrillerBus.h>
|
||||
|
||||
#define VS_VERSION_INFO 1
|
||||
#define IDD_CRITICAL_ERROR 101
|
||||
#define IDB_CONFIRM_SAVE 102
|
||||
#define IDB_DONT_SAVE 103
|
||||
#define IDD_CONFIRM_SAVE_LEVEL 127
|
||||
#define IDB_CRASH_FACE 128
|
||||
#define IDD_EXCEPTION 245
|
||||
#define IDC_CALLSTACK 1001
|
||||
#define IDC_EXCEPTION_CODE 1002
|
||||
#define IDC_EXCEPTION_ADDRESS 1003
|
||||
#define IDC_EXCEPTION_MODULE 1004
|
||||
#define IDC_EXCEPTION_DESC 1005
|
||||
#define IDB_EXIT 1008
|
||||
#define IDB_IGNORE 1010
|
||||
__pragma(comment(lib, "version.lib"))
|
||||
|
||||
//! Needs one external of DLL handle.
|
||||
extern HMODULE gDLLHandle;
|
||||
|
||||
#include <DbgHelp.h>
|
||||
|
||||
#define MAX_PATH_LENGTH 1024
|
||||
#define MAX_SYMBOL_LENGTH 512
|
||||
|
||||
static HWND hwndException = 0;
|
||||
static bool g_bUserDialog = true; // true=on crash show dialog box, false=supress user interaction
|
||||
|
||||
static int PrintException(EXCEPTION_POINTERS* pex);
|
||||
|
||||
static bool IsFloatingPointException(EXCEPTION_POINTERS* pex);
|
||||
|
||||
extern LONG WINAPI CryEngineExceptionFilterWER(struct _EXCEPTION_POINTERS* pExceptionPointers);
|
||||
extern LONG WINAPI CryEngineExceptionFilterMiniDump(struct _EXCEPTION_POINTERS* pExceptionPointers, const char* szDumpPath, MINIDUMP_TYPE mdumpValue);
|
||||
|
||||
//=============================================================================
|
||||
CONTEXT CaptureCurrentContext()
|
||||
{
|
||||
CONTEXT context;
|
||||
memset(&context, 0, sizeof(context));
|
||||
context.ContextFlags = CONTEXT_FULL;
|
||||
RtlCaptureContext(&context);
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
LONG __stdcall CryUnhandledExceptionHandler(EXCEPTION_POINTERS* pex)
|
||||
{
|
||||
return DebugCallStack::instance()->handleException(pex);
|
||||
}
|
||||
|
||||
|
||||
BOOL CALLBACK EnumModules(
|
||||
PCSTR ModuleName,
|
||||
DWORD64 BaseOfDll,
|
||||
PVOID UserContext)
|
||||
{
|
||||
DebugCallStack::TModules& modules = *static_cast<DebugCallStack::TModules*>(UserContext);
|
||||
modules[(void*)BaseOfDll] = ModuleName;
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
//=============================================================================
|
||||
// Class Statics
|
||||
//=============================================================================
|
||||
|
||||
// Return single instance of class.
|
||||
IDebugCallStack* IDebugCallStack::instance()
|
||||
{
|
||||
static DebugCallStack sInstance;
|
||||
return &sInstance;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------------------------------------------------
|
||||
// Sets up the symbols for functions in the debug file.
|
||||
//------------------------------------------------------------------------------------------------------------------------
|
||||
DebugCallStack::DebugCallStack()
|
||||
: prevExceptionHandler(0)
|
||||
, m_pSystem(0)
|
||||
, m_nSkipNumFunctions(0)
|
||||
, m_bCrash(false)
|
||||
, m_szBugMessage(NULL)
|
||||
{
|
||||
}
|
||||
|
||||
DebugCallStack::~DebugCallStack()
|
||||
{
|
||||
}
|
||||
|
||||
void DebugCallStack::RemoveOldFiles()
|
||||
{
|
||||
RemoveFile("error.log");
|
||||
RemoveFile("error.bmp");
|
||||
RemoveFile("error.dmp");
|
||||
}
|
||||
|
||||
void DebugCallStack::RemoveFile(const char* szFileName)
|
||||
{
|
||||
FILE* pFile = nullptr;
|
||||
azfopen(&pFile, szFileName, "r");
|
||||
const bool bFileExists = (pFile != NULL);
|
||||
|
||||
if (bFileExists)
|
||||
{
|
||||
fclose(pFile);
|
||||
|
||||
WriteLineToLog("Removing file \"%s\"...", szFileName);
|
||||
if (remove(szFileName) == 0)
|
||||
{
|
||||
WriteLineToLog("File successfully removed.");
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteLineToLog("Couldn't remove file!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DebugCallStack::installErrorHandler(ISystem* pSystem)
|
||||
{
|
||||
m_pSystem = pSystem;
|
||||
prevExceptionHandler = (void*)SetUnhandledExceptionFilter(CryUnhandledExceptionHandler);
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
void DebugCallStack::SetUserDialogEnable(const bool bUserDialogEnable)
|
||||
{
|
||||
g_bUserDialog = bUserDialogEnable;
|
||||
}
|
||||
|
||||
|
||||
DWORD g_idDebugThreads[10];
|
||||
const char* g_nameDebugThreads[10];
|
||||
int g_nDebugThreads = 0;
|
||||
volatile int g_lockThreadDumpList = 0;
|
||||
|
||||
void MarkThisThreadForDebugging(const char* name)
|
||||
{
|
||||
EBUS_EVENT(AZ::Debug::EventTraceDrillerSetupBus, SetThreadName, AZStd::this_thread::get_id(), name);
|
||||
|
||||
WriteLock lock(g_lockThreadDumpList);
|
||||
DWORD id = GetCurrentThreadId();
|
||||
if (g_nDebugThreads == sizeof(g_idDebugThreads) / sizeof(g_idDebugThreads[0]))
|
||||
{
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < g_nDebugThreads; i++)
|
||||
{
|
||||
if (g_idDebugThreads[i] == id)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
g_nameDebugThreads[g_nDebugThreads] = name;
|
||||
g_idDebugThreads[g_nDebugThreads++] = id;
|
||||
((CSystem*)gEnv->pSystem)->EnableFloatExceptions(g_cvars.sys_float_exceptions);
|
||||
}
|
||||
|
||||
void UnmarkThisThreadFromDebugging()
|
||||
{
|
||||
WriteLock lock(g_lockThreadDumpList);
|
||||
DWORD id = GetCurrentThreadId();
|
||||
for (int i = g_nDebugThreads - 1; i >= 0; i--)
|
||||
{
|
||||
if (g_idDebugThreads[i] == id)
|
||||
{
|
||||
memmove(g_idDebugThreads + i, g_idDebugThreads + i + 1, (g_nDebugThreads - 1 - i) * sizeof(g_idDebugThreads[0]));
|
||||
memmove(g_nameDebugThreads + i, g_nameDebugThreads + i + 1, (g_nDebugThreads - 1 - i) * sizeof(g_nameDebugThreads[0]));
|
||||
--g_nDebugThreads;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extern int prev_sys_float_exceptions;
|
||||
void UpdateFPExceptionsMaskForThreads()
|
||||
{
|
||||
int mask = -iszero(g_cvars.sys_float_exceptions);
|
||||
CONTEXT ctx;
|
||||
for (int i = 0; i < g_nDebugThreads; i++)
|
||||
{
|
||||
if (g_idDebugThreads[i] != GetCurrentThreadId())
|
||||
{
|
||||
HANDLE hThread = OpenThread(THREAD_ALL_ACCESS, TRUE, g_idDebugThreads[i]);
|
||||
ctx.ContextFlags = CONTEXT_ALL;
|
||||
SuspendThread(hThread);
|
||||
GetThreadContext(hThread, &ctx);
|
||||
#ifndef WIN64
|
||||
(ctx.FloatSave.ControlWord |= 7) &= ~5 | mask;
|
||||
(*(WORD*)(ctx.ExtendedRegisters + 24) |= 0x280) &= ~0x280 | mask;
|
||||
#else
|
||||
(ctx.FltSave.ControlWord |= 7) &= ~5 | mask;
|
||||
(ctx.FltSave.MxCsr |= 0x280) &= ~0x280 | mask;
|
||||
#endif
|
||||
SetThreadContext(hThread, &ctx);
|
||||
ResumeThread(hThread);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
int DebugCallStack::handleException(EXCEPTION_POINTERS* exception_pointer)
|
||||
{
|
||||
if (gEnv == NULL)
|
||||
{
|
||||
return EXCEPTION_EXECUTE_HANDLER;
|
||||
}
|
||||
|
||||
ResetFPU(exception_pointer);
|
||||
|
||||
prev_sys_float_exceptions = 0;
|
||||
const int cached_sys_float_exceptions = g_cvars.sys_float_exceptions;
|
||||
|
||||
((CSystem*)gEnv->pSystem)->EnableFloatExceptions(0);
|
||||
|
||||
if (g_cvars.sys_WER)
|
||||
{
|
||||
gEnv->pLog->FlushAndClose();
|
||||
return CryEngineExceptionFilterWER(exception_pointer);
|
||||
}
|
||||
|
||||
if (g_cvars.sys_no_crash_dialog)
|
||||
{
|
||||
DWORD dwMode = SetErrorMode(SEM_NOGPFAULTERRORBOX);
|
||||
SetErrorMode(dwMode | SEM_NOGPFAULTERRORBOX);
|
||||
}
|
||||
|
||||
m_bCrash = true;
|
||||
|
||||
if (g_cvars.sys_no_crash_dialog)
|
||||
{
|
||||
DWORD dwMode = SetErrorMode(SEM_NOGPFAULTERRORBOX);
|
||||
SetErrorMode(dwMode | SEM_NOGPFAULTERRORBOX);
|
||||
}
|
||||
|
||||
static bool firstTime = true;
|
||||
|
||||
if (g_cvars.sys_dump_aux_threads)
|
||||
{
|
||||
for (int i = 0; i < g_nDebugThreads; i++)
|
||||
{
|
||||
if (g_idDebugThreads[i] != GetCurrentThreadId())
|
||||
{
|
||||
SuspendThread(OpenThread(THREAD_ALL_ACCESS, TRUE, g_idDebugThreads[i]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// uninstall our exception handler.
|
||||
SetUnhandledExceptionFilter((LPTOP_LEVEL_EXCEPTION_FILTER)prevExceptionHandler);
|
||||
|
||||
if (!firstTime)
|
||||
{
|
||||
WriteLineToLog("Critical Exception! Called Multiple Times!");
|
||||
gEnv->pLog->FlushAndClose();
|
||||
// Exception called more then once.
|
||||
return EXCEPTION_EXECUTE_HANDLER;
|
||||
}
|
||||
|
||||
// Print exception info:
|
||||
{
|
||||
char excCode[80];
|
||||
char excAddr[80];
|
||||
WriteLineToLog("<CRITICAL EXCEPTION>");
|
||||
sprintf_s(excAddr, "0x%04X:0x%p", exception_pointer->ContextRecord->SegCs, exception_pointer->ExceptionRecord->ExceptionAddress);
|
||||
sprintf_s(excCode, "0x%08X", exception_pointer->ExceptionRecord->ExceptionCode);
|
||||
WriteLineToLog("Exception: %s, at Address: %s", excCode, excAddr);
|
||||
}
|
||||
|
||||
firstTime = false;
|
||||
|
||||
const int ret = SubmitBug(exception_pointer);
|
||||
|
||||
if (ret != IDB_IGNORE)
|
||||
{
|
||||
CryEngineExceptionFilterWER(exception_pointer);
|
||||
}
|
||||
|
||||
gEnv->pLog->FlushAndClose();
|
||||
|
||||
if (exception_pointer->ExceptionRecord->ExceptionFlags & EXCEPTION_NONCONTINUABLE)
|
||||
{
|
||||
// This is non continuable exception. abort application now.
|
||||
exit(exception_pointer->ExceptionRecord->ExceptionCode);
|
||||
}
|
||||
|
||||
//typedef long (__stdcall *ExceptionFunc)(EXCEPTION_POINTERS*);
|
||||
//ExceptionFunc prevFunc = (ExceptionFunc)prevExceptionHandler;
|
||||
//return prevFunc( (EXCEPTION_POINTERS*)exception_pointer );
|
||||
if (ret == IDB_EXIT)
|
||||
{
|
||||
// Immediate exit.
|
||||
// on windows, exit() and _exit() do all sorts of things, unfortuantely
|
||||
// TerminateProcess is the only way to die.
|
||||
TerminateProcess(GetCurrentProcess(), exception_pointer->ExceptionRecord->ExceptionCode); // we crashed, so don't return a zero exit code!
|
||||
// on linux based systems, _exit will not call ATEXIT and other things, which makes it more suitable for termination in an emergency such
|
||||
// as an unhandled exception.
|
||||
// however, this function is a windows exception handler.
|
||||
}
|
||||
else if (ret == IDB_IGNORE)
|
||||
{
|
||||
#ifndef WIN64
|
||||
exception_pointer->ContextRecord->FloatSave.StatusWord &= ~31;
|
||||
exception_pointer->ContextRecord->FloatSave.ControlWord |= 7;
|
||||
(*(WORD*)(exception_pointer->ContextRecord->ExtendedRegisters + 24) &= 31) |= 0x1F80;
|
||||
#else
|
||||
exception_pointer->ContextRecord->FltSave.StatusWord &= ~31;
|
||||
exception_pointer->ContextRecord->FltSave.ControlWord |= 7;
|
||||
(exception_pointer->ContextRecord->FltSave.MxCsr &= 31) |= 0x1F80;
|
||||
#endif
|
||||
firstTime = true;
|
||||
prevExceptionHandler = (void*)SetUnhandledExceptionFilter(CryUnhandledExceptionHandler);
|
||||
g_cvars.sys_float_exceptions = cached_sys_float_exceptions;
|
||||
((CSystem*)gEnv->pSystem)->EnableFloatExceptions(g_cvars.sys_float_exceptions);
|
||||
return EXCEPTION_CONTINUE_EXECUTION;
|
||||
}
|
||||
|
||||
// Continue;
|
||||
return EXCEPTION_EXECUTE_HANDLER;
|
||||
}
|
||||
|
||||
void DebugCallStack::ReportBug(const char* szErrorMessage)
|
||||
{
|
||||
WriteLineToLog("Reporting bug: %s", szErrorMessage);
|
||||
|
||||
m_szBugMessage = szErrorMessage;
|
||||
m_context = CaptureCurrentContext();
|
||||
SubmitBug(NULL);
|
||||
m_szBugMessage = NULL;
|
||||
}
|
||||
|
||||
void DebugCallStack::dumpCallStack(std::vector<string>& funcs)
|
||||
{
|
||||
WriteLineToLog("=============================================================================");
|
||||
int len = (int)funcs.size();
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
const char* str = funcs[i].c_str();
|
||||
WriteLineToLog("%2d) %s", len - i, str);
|
||||
}
|
||||
WriteLineToLog("=============================================================================");
|
||||
}
|
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
void DebugCallStack::LogExceptionInfo(EXCEPTION_POINTERS* pex)
|
||||
{
|
||||
string path("");
|
||||
if ((gEnv) && (gEnv->pFileIO))
|
||||
{
|
||||
const char* logAlias = gEnv->pFileIO->GetAlias("@log@");
|
||||
if (!logAlias)
|
||||
{
|
||||
logAlias = gEnv->pFileIO->GetAlias("@root@");
|
||||
}
|
||||
if (logAlias)
|
||||
{
|
||||
path = logAlias;
|
||||
path += "/";
|
||||
}
|
||||
}
|
||||
|
||||
string fileName = path;
|
||||
fileName += "error.log";
|
||||
|
||||
struct stat fileInfo;
|
||||
string timeStamp;
|
||||
string backupPath;
|
||||
if (gEnv->IsDedicated())
|
||||
{
|
||||
backupPath = PathUtil::ToUnixPath(PathUtil::AddSlash(path + "DumpBackups"));
|
||||
gEnv->pFileIO->CreatePath(backupPath.c_str());
|
||||
|
||||
if (stat(fileName.c_str(), &fileInfo) == 0)
|
||||
{
|
||||
// Backup log
|
||||
tm creationTime;
|
||||
localtime_s(&creationTime, &fileInfo.st_mtime);
|
||||
char tempBuffer[32];
|
||||
strftime(tempBuffer, sizeof(tempBuffer), "%d %b %Y (%H %M %S)", &creationTime);
|
||||
timeStamp = tempBuffer;
|
||||
|
||||
string backupFileName = backupPath + timeStamp + " error.log";
|
||||
CopyFile(fileName.c_str(), backupFileName.c_str(), true);
|
||||
}
|
||||
}
|
||||
|
||||
FILE* f = nullptr;
|
||||
azfopen(&f, fileName.c_str(), "wt");
|
||||
|
||||
static char errorString[s_iCallStackSize];
|
||||
errorString[0] = 0;
|
||||
|
||||
// Time and Version.
|
||||
char versionbuf[1024];
|
||||
azstrcpy(versionbuf, AZ_ARRAY_SIZE(versionbuf), "");
|
||||
PutVersion(versionbuf, AZ_ARRAY_SIZE(versionbuf));
|
||||
cry_strcat(errorString, versionbuf);
|
||||
cry_strcat(errorString, "\n");
|
||||
|
||||
char excCode[MAX_WARNING_LENGTH];
|
||||
char excAddr[80];
|
||||
char desc[1024];
|
||||
char excDesc[MAX_WARNING_LENGTH];
|
||||
|
||||
// make sure the mouse cursor is visible
|
||||
ShowCursor(TRUE);
|
||||
|
||||
const char* excName;
|
||||
if (m_bIsFatalError || !pex)
|
||||
{
|
||||
const char* const szMessage = m_bIsFatalError ? s_szFatalErrorCode : m_szBugMessage;
|
||||
excName = szMessage;
|
||||
cry_strcpy(excCode, szMessage);
|
||||
cry_strcpy(excAddr, "");
|
||||
cry_strcpy(desc, "");
|
||||
cry_strcpy(m_excModule, "");
|
||||
cry_strcpy(excDesc, szMessage);
|
||||
}
|
||||
else
|
||||
{
|
||||
sprintf_s(excAddr, "0x%04X:0x%p", pex->ContextRecord->SegCs, pex->ExceptionRecord->ExceptionAddress);
|
||||
sprintf_s(excCode, "0x%08X", pex->ExceptionRecord->ExceptionCode);
|
||||
excName = TranslateExceptionCode(pex->ExceptionRecord->ExceptionCode);
|
||||
cry_strcpy(desc, "");
|
||||
sprintf_s(excDesc, "%s\r\n%s", excName, desc);
|
||||
|
||||
|
||||
if (pex->ExceptionRecord->ExceptionCode == EXCEPTION_ACCESS_VIOLATION)
|
||||
{
|
||||
if (pex->ExceptionRecord->NumberParameters > 1)
|
||||
{
|
||||
ULONG_PTR iswrite = pex->ExceptionRecord->ExceptionInformation[0];
|
||||
DWORD64 accessAddr = pex->ExceptionRecord->ExceptionInformation[1];
|
||||
if (iswrite)
|
||||
{
|
||||
sprintf_s(desc, "Attempt to write data to address 0x%08llu\r\nThe memory could not be \"written\"", accessAddr);
|
||||
}
|
||||
else
|
||||
{
|
||||
sprintf_s(desc, "Attempt to read from address 0x%08llu\r\nThe memory could not be \"read\"", accessAddr);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
WriteLineToLog("Exception Code: %s", excCode);
|
||||
WriteLineToLog("Exception Addr: %s", excAddr);
|
||||
WriteLineToLog("Exception Module: %s", m_excModule);
|
||||
WriteLineToLog("Exception Name : %s", excName);
|
||||
WriteLineToLog("Exception Description: %s", desc);
|
||||
|
||||
|
||||
cry_strcpy(m_excDesc, excDesc);
|
||||
cry_strcpy(m_excAddr, excAddr);
|
||||
cry_strcpy(m_excCode, excCode);
|
||||
|
||||
|
||||
char errs[32768];
|
||||
sprintf_s(errs, "Exception Code: %s\nException Addr: %s\nException Module: %s\nException Description: %s, %s\n",
|
||||
excCode, excAddr, m_excModule, excName, desc);
|
||||
|
||||
|
||||
cry_strcat(errs, "\nCall Stack Trace:\n");
|
||||
|
||||
std::vector<string> funcs;
|
||||
{
|
||||
AZ::Debug::StackFrame frames[25];
|
||||
AZ::Debug::SymbolStorage::StackLine lines[AZ_ARRAY_SIZE(frames)];
|
||||
unsigned int numFrames = AZ::Debug::StackRecorder::Record(frames, AZ_ARRAY_SIZE(frames), 3);
|
||||
if (numFrames)
|
||||
{
|
||||
AZ::Debug::SymbolStorage::DecodeFrames(frames, numFrames, lines);
|
||||
for (unsigned int i = 0; i < numFrames; i++)
|
||||
{
|
||||
funcs.push_back(lines[i]);
|
||||
}
|
||||
}
|
||||
dumpCallStack(funcs);
|
||||
// Fill call stack.
|
||||
char str[s_iCallStackSize];
|
||||
cry_strcpy(str, "");
|
||||
for (unsigned int i = 0; i < funcs.size(); i++)
|
||||
{
|
||||
char temp[s_iCallStackSize];
|
||||
sprintf_s(temp, "%2zd) %s", funcs.size() - i, (const char*)funcs[i].c_str());
|
||||
cry_strcat(str, temp);
|
||||
cry_strcat(str, "\r\n");
|
||||
cry_strcat(errs, temp);
|
||||
cry_strcat(errs, "\n");
|
||||
}
|
||||
cry_strcpy(m_excCallstack, str);
|
||||
}
|
||||
|
||||
cry_strcat(errorString, errs);
|
||||
|
||||
if (f)
|
||||
{
|
||||
fwrite(errorString, strlen(errorString), 1, f);
|
||||
{
|
||||
if (g_cvars.sys_dump_aux_threads)
|
||||
{
|
||||
for (int i = 0; i < g_nDebugThreads; i++)
|
||||
{
|
||||
if (g_idDebugThreads[i] != GetCurrentThreadId())
|
||||
{
|
||||
fprintf(f, "\n\nSuspended thread (%s):\n", g_nameDebugThreads[i]);
|
||||
HANDLE hThread = OpenThread(THREAD_ALL_ACCESS, TRUE, g_idDebugThreads[i]);
|
||||
|
||||
// mirrors the AZ::Debug::Trace::PrintCallstack() functionality, but prints to a file
|
||||
{
|
||||
AZ::Debug::StackFrame frames[10];
|
||||
|
||||
// Without StackFrame explicit alignment frames array is aligned to 4 bytes
|
||||
// which causes the stack tracing to fail.
|
||||
AZ::Debug::SymbolStorage::StackLine lines[AZ_ARRAY_SIZE(frames)];
|
||||
|
||||
unsigned int numFrames = AZ::Debug::StackRecorder::Record(frames, AZ_ARRAY_SIZE(frames), 0, hThread);
|
||||
if (numFrames)
|
||||
{
|
||||
AZ::Debug::SymbolStorage::DecodeFrames(frames, numFrames, lines);
|
||||
for (unsigned int i2 = 0; i2 < numFrames; ++i2)
|
||||
{
|
||||
fprintf(f, "%2d) %s\n", numFrames - i2, lines[i2]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ResumeThread(hThread);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fflush(f);
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
if (pex)
|
||||
{
|
||||
MINIDUMP_TYPE mdumpValue;
|
||||
bool bDump = true;
|
||||
switch (g_cvars.sys_dump_type)
|
||||
{
|
||||
case 0:
|
||||
bDump = false;
|
||||
break;
|
||||
case 1:
|
||||
mdumpValue = MiniDumpNormal;
|
||||
break;
|
||||
case 2:
|
||||
mdumpValue = (MINIDUMP_TYPE)(MiniDumpWithIndirectlyReferencedMemory | MiniDumpWithDataSegs);
|
||||
break;
|
||||
case 3:
|
||||
mdumpValue = MiniDumpWithFullMemory;
|
||||
break;
|
||||
default:
|
||||
mdumpValue = (MINIDUMP_TYPE)g_cvars.sys_dump_type;
|
||||
break;
|
||||
}
|
||||
if (bDump)
|
||||
{
|
||||
fileName = path + "error.dmp";
|
||||
|
||||
if (gEnv->IsDedicated() && stat(fileName.c_str(), &fileInfo) == 0)
|
||||
{
|
||||
// Backup dump (use timestamp from error.log if available)
|
||||
if (timeStamp.empty())
|
||||
{
|
||||
tm creationTime;
|
||||
localtime_s(&creationTime, &fileInfo.st_mtime);
|
||||
char tempBuffer[32];
|
||||
strftime(tempBuffer, sizeof(tempBuffer), "%d %b %Y (%H %M %S)", &creationTime);
|
||||
timeStamp = tempBuffer;
|
||||
}
|
||||
|
||||
string backupFileName = backupPath + timeStamp + " error.dmp";
|
||||
CopyFile(fileName.c_str(), backupFileName.c_str(), true);
|
||||
}
|
||||
|
||||
CryEngineExceptionFilterMiniDump(pex, fileName.c_str(), mdumpValue);
|
||||
}
|
||||
}
|
||||
|
||||
//if no crash dialog don't even submit the bug
|
||||
if (m_postBackupProcess && g_cvars.sys_no_crash_dialog == 0 && g_bUserDialog)
|
||||
{
|
||||
m_postBackupProcess();
|
||||
}
|
||||
else
|
||||
{
|
||||
// lawsonn: Disabling the JIRA-based crash reporter for now
|
||||
// we'll need to deal with it our own way, pending QA.
|
||||
// if you're customizing the engine this is also your opportunity to deal with it.
|
||||
if (g_cvars.sys_no_crash_dialog != 0 || !g_bUserDialog)
|
||||
{
|
||||
// ------------ place custom crash handler here ---------------------
|
||||
// it should launch an executable!
|
||||
/// by this time, error.bmp will be in the engine root folder
|
||||
// error.log and error.dmp will also be present in the engine root folder
|
||||
// if your error dumper wants those, it should zip them up and send them or offer to do so.
|
||||
// ------------------------------------------------------------------
|
||||
}
|
||||
}
|
||||
const bool bQuitting = !gEnv || !gEnv->pSystem || gEnv->pSystem->IsQuitting();
|
||||
|
||||
//[AlexMcC|16.04.10] When the engine is shutting down, MessageBox doesn't display a box
|
||||
// and immediately returns IDYES. Avoid this by just not trying to save if we're quitting.
|
||||
// Don't ask to save if this isn't a real crash (a real crash has exception pointers)
|
||||
if (g_cvars.sys_no_crash_dialog == 0 && g_bUserDialog && gEnv->IsEditor() && !bQuitting && pex)
|
||||
{
|
||||
BackupCurrentLevel();
|
||||
|
||||
const INT_PTR res = DialogBoxParam(gDLLHandle, MAKEINTRESOURCE(IDD_CONFIRM_SAVE_LEVEL), NULL, DebugCallStack::ConfirmSaveDialogProc, NULL);
|
||||
if (res == IDB_CONFIRM_SAVE)
|
||||
{
|
||||
if (SaveCurrentLevel())
|
||||
{
|
||||
MessageBox(NULL, "Level has been successfully saved!\r\nPress Ok to terminate Editor.", "Save", MB_OK);
|
||||
}
|
||||
else
|
||||
{
|
||||
MessageBox(NULL, "Error saving level.\r\nPress Ok to terminate Editor.", "Save", MB_OK | MB_ICONWARNING);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (g_cvars.sys_no_crash_dialog != 0 || !g_bUserDialog)
|
||||
{
|
||||
// terminate immediately - since we're in a crash, there is no point unwinding stack, we've already done access violation or worse.
|
||||
// calling exit will only cause further death down the line...
|
||||
TerminateProcess(GetCurrentProcess(), pex->ExceptionRecord->ExceptionCode);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
INT_PTR CALLBACK DebugCallStack::ExceptionDialogProc(HWND hwndDlg, UINT message, WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
static EXCEPTION_POINTERS* pex;
|
||||
|
||||
static char errorString[32768] = "";
|
||||
|
||||
switch (message)
|
||||
{
|
||||
case WM_INITDIALOG:
|
||||
{
|
||||
pex = (EXCEPTION_POINTERS*)lParam;
|
||||
HWND h;
|
||||
|
||||
if (pex->ExceptionRecord->ExceptionFlags & EXCEPTION_NONCONTINUABLE)
|
||||
{
|
||||
// Disable continue button for non continuable exceptions.
|
||||
//h = GetDlgItem( hwndDlg,IDB_CONTINUE );
|
||||
//if (h) EnableWindow( h,FALSE );
|
||||
}
|
||||
|
||||
DebugCallStack* pDCS = static_cast<DebugCallStack*>(DebugCallStack::instance());
|
||||
|
||||
h = GetDlgItem(hwndDlg, IDC_EXCEPTION_DESC);
|
||||
if (h)
|
||||
{
|
||||
SendMessage(h, EM_REPLACESEL, FALSE, (LONG_PTR)pDCS->m_excDesc);
|
||||
}
|
||||
|
||||
h = GetDlgItem(hwndDlg, IDC_EXCEPTION_CODE);
|
||||
if (h)
|
||||
{
|
||||
SendMessage(h, EM_REPLACESEL, FALSE, (LONG_PTR)pDCS->m_excCode);
|
||||
}
|
||||
|
||||
h = GetDlgItem(hwndDlg, IDC_EXCEPTION_MODULE);
|
||||
if (h)
|
||||
{
|
||||
SendMessage(h, EM_REPLACESEL, FALSE, (LONG_PTR)pDCS->m_excModule);
|
||||
}
|
||||
|
||||
h = GetDlgItem(hwndDlg, IDC_EXCEPTION_ADDRESS);
|
||||
if (h)
|
||||
{
|
||||
SendMessage(h, EM_REPLACESEL, FALSE, (LONG_PTR)pDCS->m_excAddr);
|
||||
}
|
||||
|
||||
// Fill call stack.
|
||||
HWND callStack = GetDlgItem(hwndDlg, IDC_CALLSTACK);
|
||||
if (callStack)
|
||||
{
|
||||
SendMessage(callStack, WM_SETTEXT, FALSE, (LPARAM)pDCS->m_excCallstack);
|
||||
}
|
||||
|
||||
if (hwndException)
|
||||
{
|
||||
DestroyWindow(hwndException);
|
||||
hwndException = 0;
|
||||
}
|
||||
|
||||
if (IsFloatingPointException(pex))
|
||||
{
|
||||
EnableWindow(GetDlgItem(hwndDlg, IDB_IGNORE), TRUE);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case WM_COMMAND:
|
||||
switch (LOWORD(wParam))
|
||||
{
|
||||
case IDB_EXIT:
|
||||
case IDB_IGNORE:
|
||||
// Fall through.
|
||||
|
||||
EndDialog(hwndDlg, wParam);
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
INT_PTR CALLBACK DebugCallStack::ConfirmSaveDialogProc(HWND hwndDlg, UINT message, WPARAM wParam, [[maybe_unused]] LPARAM lParam)
|
||||
{
|
||||
switch (message)
|
||||
{
|
||||
case WM_INITDIALOG:
|
||||
{
|
||||
// The user might be holding down the spacebar while the engine crashes.
|
||||
// If we don't remove keyboard focus from this dialog, the keypress will
|
||||
// press the default button before the dialog actually appears, even if
|
||||
// the user has already released the key, which is bad.
|
||||
SetFocus(NULL);
|
||||
} break;
|
||||
case WM_COMMAND:
|
||||
{
|
||||
switch (LOWORD(wParam))
|
||||
{
|
||||
case IDB_CONFIRM_SAVE: // Fall through
|
||||
case IDB_DONT_SAVE:
|
||||
{
|
||||
EndDialog(hwndDlg, wParam);
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
} break;
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
bool DebugCallStack::BackupCurrentLevel()
|
||||
{
|
||||
CSystem* pSystem = static_cast<CSystem*>(m_pSystem);
|
||||
if (pSystem && pSystem->GetUserCallback())
|
||||
{
|
||||
return pSystem->GetUserCallback()->OnBackupDocument();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool DebugCallStack::SaveCurrentLevel()
|
||||
{
|
||||
CSystem* pSystem = static_cast<CSystem*>(m_pSystem);
|
||||
if (pSystem && pSystem->GetUserCallback())
|
||||
{
|
||||
return pSystem->GetUserCallback()->OnSaveDocument();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
int DebugCallStack::SubmitBug(EXCEPTION_POINTERS* exception_pointer)
|
||||
{
|
||||
int ret = IDB_EXIT;
|
||||
|
||||
assert(!hwndException);
|
||||
|
||||
RemoveOldFiles();
|
||||
|
||||
AZ::Debug::Trace::PrintCallstack("", 2);
|
||||
|
||||
LogExceptionInfo(exception_pointer);
|
||||
|
||||
if (IsFloatingPointException(exception_pointer))
|
||||
{
|
||||
//! Print exception dialog.
|
||||
ret = PrintException(exception_pointer);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
void DebugCallStack::ResetFPU(EXCEPTION_POINTERS* pex)
|
||||
{
|
||||
if (IsFloatingPointException(pex))
|
||||
{
|
||||
// How to reset FPU: http://www.experts-exchange.com/Programming/System/Windows__Programming/Q_10310953.html
|
||||
_clearfp();
|
||||
#ifndef WIN64
|
||||
pex->ContextRecord->FloatSave.ControlWord |= 0x2F;
|
||||
pex->ContextRecord->FloatSave.StatusWord &= ~0x8080;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
string DebugCallStack::GetModuleNameForAddr(void* addr)
|
||||
{
|
||||
if (m_modules.empty())
|
||||
{
|
||||
return "[unknown]";
|
||||
}
|
||||
|
||||
if (addr < m_modules.begin()->first)
|
||||
{
|
||||
return "[unknown]";
|
||||
}
|
||||
|
||||
TModules::const_iterator it = m_modules.begin();
|
||||
TModules::const_iterator end = m_modules.end();
|
||||
for (; ++it != end; )
|
||||
{
|
||||
if (addr < it->first)
|
||||
{
|
||||
return (--it)->second;
|
||||
}
|
||||
}
|
||||
|
||||
//if address is higher than the last module, we simply assume it is in the last module.
|
||||
return m_modules.rbegin()->second;
|
||||
}
|
||||
|
||||
void DebugCallStack::GetProcNameForAddr(void* addr, string& procName, void*& baseAddr, string& filename, int& line)
|
||||
{
|
||||
AZ::Debug::SymbolStorage::StackLine func, file, module;
|
||||
AZ::Debug::SymbolStorage::FindFunctionFromIP(addr, &func, &file, &module, line, baseAddr);
|
||||
procName = func;
|
||||
filename = file;
|
||||
}
|
||||
|
||||
string DebugCallStack::GetCurrentFilename()
|
||||
{
|
||||
char fullpath[MAX_PATH_LENGTH + 1];
|
||||
GetModuleFileName(NULL, fullpath, MAX_PATH_LENGTH);
|
||||
return fullpath;
|
||||
}
|
||||
|
||||
static bool IsFloatingPointException(EXCEPTION_POINTERS* pex)
|
||||
{
|
||||
if (!pex)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
DWORD exceptionCode = pex->ExceptionRecord->ExceptionCode;
|
||||
switch (exceptionCode)
|
||||
{
|
||||
case EXCEPTION_FLT_DENORMAL_OPERAND:
|
||||
case EXCEPTION_FLT_DIVIDE_BY_ZERO:
|
||||
case EXCEPTION_FLT_INEXACT_RESULT:
|
||||
case EXCEPTION_FLT_INVALID_OPERATION:
|
||||
case EXCEPTION_FLT_OVERFLOW:
|
||||
case EXCEPTION_FLT_UNDERFLOW:
|
||||
case STATUS_FLOAT_MULTIPLE_FAULTS:
|
||||
case STATUS_FLOAT_MULTIPLE_TRAPS:
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
int DebugCallStack::PrintException(EXCEPTION_POINTERS* exception_pointer)
|
||||
{
|
||||
return (int)DialogBoxParam(gDLLHandle, MAKEINTRESOURCE(IDD_CRITICAL_ERROR), NULL, DebugCallStack::ExceptionDialogProc, (LPARAM)exception_pointer);
|
||||
}
|
||||
|
||||
#else
|
||||
void MarkThisThreadForDebugging(const char*) {}
|
||||
void UnmarkThisThreadFromDebugging() {}
|
||||
void UpdateFPExceptionsMaskForThreads() {}
|
||||
#endif //WIN32
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
// Original file Copyright Crytek GMBH or its affiliates, used under license.
|
||||
|
||||
#ifndef CRYINCLUDE_CRYSYSTEM_DEBUGCALLSTACK_H
|
||||
#define CRYINCLUDE_CRYSYSTEM_DEBUGCALLSTACK_H
|
||||
#pragma once
|
||||
|
||||
|
||||
#include "IDebugCallStack.h"
|
||||
|
||||
#if defined (WIN32) || defined (WIN64)
|
||||
|
||||
//! Limits the maximal number of functions in call stack.
|
||||
const int MAX_DEBUG_STACK_ENTRIES_FILE_DUMP = 12;
|
||||
|
||||
struct ISystem;
|
||||
|
||||
//!============================================================================
|
||||
//!
|
||||
//! DebugCallStack class, capture call stack information from symbol files.
|
||||
//!
|
||||
//!============================================================================
|
||||
class DebugCallStack
|
||||
: public IDebugCallStack
|
||||
{
|
||||
public:
|
||||
DebugCallStack();
|
||||
virtual ~DebugCallStack();
|
||||
|
||||
ISystem* GetSystem() { return m_pSystem; };
|
||||
|
||||
virtual string GetModuleNameForAddr(void* addr);
|
||||
virtual void GetProcNameForAddr(void* addr, string& procName, void*& baseAddr, string& filename, int& line);
|
||||
virtual string GetCurrentFilename();
|
||||
|
||||
void installErrorHandler(ISystem* pSystem);
|
||||
virtual int handleException(EXCEPTION_POINTERS* exception_pointer);
|
||||
|
||||
virtual void ReportBug(const char*);
|
||||
|
||||
void dumpCallStack(std::vector<string>& functions);
|
||||
|
||||
void SetUserDialogEnable(const bool bUserDialogEnable);
|
||||
|
||||
typedef std::map<void*, string> TModules;
|
||||
protected:
|
||||
static void RemoveOldFiles();
|
||||
static void RemoveFile(const char* szFileName);
|
||||
|
||||
static int PrintException(EXCEPTION_POINTERS* exception_pointer);
|
||||
static INT_PTR CALLBACK ExceptionDialogProc(HWND hwndDlg, UINT message, WPARAM wParam, LPARAM lParam);
|
||||
static INT_PTR CALLBACK ConfirmSaveDialogProc(HWND hwndDlg, UINT message, WPARAM wParam, LPARAM lParam);
|
||||
|
||||
void LogExceptionInfo(EXCEPTION_POINTERS* exception_pointer);
|
||||
bool BackupCurrentLevel();
|
||||
bool SaveCurrentLevel();
|
||||
int SubmitBug(EXCEPTION_POINTERS* exception_pointer);
|
||||
void ResetFPU(EXCEPTION_POINTERS* pex);
|
||||
|
||||
static const int s_iCallStackSize = 32768;
|
||||
|
||||
char m_excLine[256];
|
||||
char m_excModule[128];
|
||||
|
||||
char m_excDesc[MAX_WARNING_LENGTH];
|
||||
char m_excCode[MAX_WARNING_LENGTH];
|
||||
char m_excAddr[80];
|
||||
char m_excCallstack[s_iCallStackSize];
|
||||
|
||||
void* prevExceptionHandler;
|
||||
|
||||
bool m_bCrash;
|
||||
const char* m_szBugMessage;
|
||||
|
||||
ISystem* m_pSystem;
|
||||
|
||||
int m_nSkipNumFunctions;
|
||||
CONTEXT m_context;
|
||||
|
||||
TModules m_modules;
|
||||
};
|
||||
|
||||
#endif //WIN32
|
||||
|
||||
#endif // CRYINCLUDE_CRYSYSTEM_DEBUGCALLSTACK_H
|
||||
@@ -14,6 +14,7 @@
|
||||
#include "CrySystem_precompiled.h"
|
||||
#include "System.h"
|
||||
#include <AZCrySystemInitLogSink.h>
|
||||
#include "DebugCallStack.h"
|
||||
|
||||
#if defined(AZ_RESTRICTED_PLATFORM)
|
||||
#undef AZ_RESTRICTED_SECTION
|
||||
@@ -87,6 +88,16 @@ CRYSYSTEM_API ISystem* CreateSystemInterface(const SSystemInitParams& startupPar
|
||||
startupParams.pUserCallback->OnSystemConnect(pSystem);
|
||||
}
|
||||
|
||||
#if defined(WIN32)
|
||||
// Environment Variable to signal we don't want to override our exception handler - our crash report system will set this
|
||||
auto envVar = AZ::Environment::FindVariable<bool>("ExceptionHandlerIsSet");
|
||||
const bool handlerIsSet = (envVar && *envVar);
|
||||
if (!handlerIsSet)
|
||||
{
|
||||
((DebugCallStack*)IDebugCallStack::instance())->installErrorHandler(pSystem);
|
||||
}
|
||||
#endif
|
||||
|
||||
bool retVal = false;
|
||||
{
|
||||
AZ::Debug::StartupLogSinkReporter<AZ::Debug::CrySystemInitLogSink> initLogSink;
|
||||
|
||||
@@ -0,0 +1,275 @@
|
||||
/*
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
// Original file Copyright Crytek GMBH or its affiliates, used under license.
|
||||
|
||||
// Description : A multiplatform base class for handling errors and collecting call stacks
|
||||
|
||||
|
||||
#include "CrySystem_precompiled.h"
|
||||
#include "IDebugCallStack.h"
|
||||
#include "System.h"
|
||||
#include <AzFramework/IO/FileOperations.h>
|
||||
#include <AzCore/NativeUI/NativeUIRequests.h>
|
||||
#include <AzCore/StringFunc/StringFunc.h>
|
||||
#include <AzCore/Utils/Utils.h>
|
||||
//#if !defined(LINUX)
|
||||
|
||||
#include <ISystem.h>
|
||||
|
||||
const char* const IDebugCallStack::s_szFatalErrorCode = "FATAL_ERROR";
|
||||
|
||||
IDebugCallStack::IDebugCallStack()
|
||||
: m_bIsFatalError(false)
|
||||
, m_postBackupProcess(0)
|
||||
, m_memAllocFileHandle(AZ::IO::InvalidHandle)
|
||||
{
|
||||
}
|
||||
|
||||
IDebugCallStack::~IDebugCallStack()
|
||||
{
|
||||
StopMemLog();
|
||||
}
|
||||
|
||||
#if AZ_LEGACY_CRYSYSTEM_TRAIT_DEBUGCALLSTACK_SINGLETON
|
||||
IDebugCallStack* IDebugCallStack::instance()
|
||||
{
|
||||
static IDebugCallStack sInstance;
|
||||
return &sInstance;
|
||||
}
|
||||
#endif
|
||||
|
||||
void IDebugCallStack::FileCreationCallback(void (* postBackupProcess)())
|
||||
{
|
||||
m_postBackupProcess = postBackupProcess;
|
||||
}
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
void IDebugCallStack::LogCallstack()
|
||||
{
|
||||
AZ::Debug::Trace::PrintCallstack("", 2);
|
||||
}
|
||||
|
||||
const char* IDebugCallStack::TranslateExceptionCode(DWORD dwExcept)
|
||||
{
|
||||
switch (dwExcept)
|
||||
{
|
||||
#if AZ_LEGACY_CRYSYSTEM_TRAIT_DEBUGCALLSTACK_TRANSLATE
|
||||
case EXCEPTION_ACCESS_VIOLATION:
|
||||
return "EXCEPTION_ACCESS_VIOLATION";
|
||||
break;
|
||||
case EXCEPTION_DATATYPE_MISALIGNMENT:
|
||||
return "EXCEPTION_DATATYPE_MISALIGNMENT";
|
||||
break;
|
||||
case EXCEPTION_BREAKPOINT:
|
||||
return "EXCEPTION_BREAKPOINT";
|
||||
break;
|
||||
case EXCEPTION_SINGLE_STEP:
|
||||
return "EXCEPTION_SINGLE_STEP";
|
||||
break;
|
||||
case EXCEPTION_ARRAY_BOUNDS_EXCEEDED:
|
||||
return "EXCEPTION_ARRAY_BOUNDS_EXCEEDED";
|
||||
break;
|
||||
case EXCEPTION_FLT_DENORMAL_OPERAND:
|
||||
return "EXCEPTION_FLT_DENORMAL_OPERAND";
|
||||
break;
|
||||
case EXCEPTION_FLT_DIVIDE_BY_ZERO:
|
||||
return "EXCEPTION_FLT_DIVIDE_BY_ZERO";
|
||||
break;
|
||||
case EXCEPTION_FLT_INEXACT_RESULT:
|
||||
return "EXCEPTION_FLT_INEXACT_RESULT";
|
||||
break;
|
||||
case EXCEPTION_FLT_INVALID_OPERATION:
|
||||
return "EXCEPTION_FLT_INVALID_OPERATION";
|
||||
break;
|
||||
case EXCEPTION_FLT_OVERFLOW:
|
||||
return "EXCEPTION_FLT_OVERFLOW";
|
||||
break;
|
||||
case EXCEPTION_FLT_STACK_CHECK:
|
||||
return "EXCEPTION_FLT_STACK_CHECK";
|
||||
break;
|
||||
case EXCEPTION_FLT_UNDERFLOW:
|
||||
return "EXCEPTION_FLT_UNDERFLOW";
|
||||
break;
|
||||
case EXCEPTION_INT_DIVIDE_BY_ZERO:
|
||||
return "EXCEPTION_INT_DIVIDE_BY_ZERO";
|
||||
break;
|
||||
case EXCEPTION_INT_OVERFLOW:
|
||||
return "EXCEPTION_INT_OVERFLOW";
|
||||
break;
|
||||
case EXCEPTION_PRIV_INSTRUCTION:
|
||||
return "EXCEPTION_PRIV_INSTRUCTION";
|
||||
break;
|
||||
case EXCEPTION_IN_PAGE_ERROR:
|
||||
return "EXCEPTION_IN_PAGE_ERROR";
|
||||
break;
|
||||
case EXCEPTION_ILLEGAL_INSTRUCTION:
|
||||
return "EXCEPTION_ILLEGAL_INSTRUCTION";
|
||||
break;
|
||||
case EXCEPTION_NONCONTINUABLE_EXCEPTION:
|
||||
return "EXCEPTION_NONCONTINUABLE_EXCEPTION";
|
||||
break;
|
||||
case EXCEPTION_STACK_OVERFLOW:
|
||||
return "EXCEPTION_STACK_OVERFLOW";
|
||||
break;
|
||||
case EXCEPTION_INVALID_DISPOSITION:
|
||||
return "EXCEPTION_INVALID_DISPOSITION";
|
||||
break;
|
||||
case EXCEPTION_GUARD_PAGE:
|
||||
return "EXCEPTION_GUARD_PAGE";
|
||||
break;
|
||||
case EXCEPTION_INVALID_HANDLE:
|
||||
return "EXCEPTION_INVALID_HANDLE";
|
||||
break;
|
||||
//case EXCEPTION_POSSIBLE_DEADLOCK: return "EXCEPTION_POSSIBLE_DEADLOCK"; break ;
|
||||
|
||||
case STATUS_FLOAT_MULTIPLE_FAULTS:
|
||||
return "STATUS_FLOAT_MULTIPLE_FAULTS";
|
||||
break;
|
||||
case STATUS_FLOAT_MULTIPLE_TRAPS:
|
||||
return "STATUS_FLOAT_MULTIPLE_TRAPS";
|
||||
break;
|
||||
|
||||
|
||||
#endif
|
||||
default:
|
||||
return "Unknown";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void IDebugCallStack::PutVersion(char* str, size_t length)
|
||||
{
|
||||
AZ_PUSH_DISABLE_WARNING(4996, "-Wunknown-warning-option")
|
||||
|
||||
if (!gEnv || !gEnv->pSystem)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
char sFileVersion[128];
|
||||
gEnv->pSystem->GetFileVersion().ToString(sFileVersion, sizeof(sFileVersion));
|
||||
|
||||
char sProductVersion[128];
|
||||
gEnv->pSystem->GetProductVersion().ToString(sProductVersion, sizeof(sFileVersion));
|
||||
|
||||
|
||||
//! Get time.
|
||||
time_t ltime;
|
||||
time(<ime);
|
||||
tm* today = localtime(<ime);
|
||||
|
||||
char s[1024];
|
||||
//! Use strftime to build a customized time string.
|
||||
strftime(s, 128, "Logged at %#c\n", today);
|
||||
azstrcat(str, length, s);
|
||||
sprintf_s(s, "FileVersion: %s\n", sFileVersion);
|
||||
azstrcat(str, length, s);
|
||||
sprintf_s(s, "ProductVersion: %s\n", sProductVersion);
|
||||
azstrcat(str, length, s);
|
||||
|
||||
if (gEnv->pLog)
|
||||
{
|
||||
const char* logfile = gEnv->pLog->GetFileName();
|
||||
if (logfile)
|
||||
{
|
||||
sprintf (s, "LogFile: %s\n", logfile);
|
||||
azstrcat(str, length, s);
|
||||
}
|
||||
}
|
||||
|
||||
AZ::IO::FixedMaxPathString projectPath = AZ::Utils::GetProjectPath();
|
||||
azstrcat(str, length, "ProjectDir: ");
|
||||
azstrcat(str, length, projectPath.c_str());
|
||||
azstrcat(str, length, "\n");
|
||||
|
||||
#if AZ_LEGACY_CRYSYSTEM_TRAIT_DEBUGCALLSTACK_APPEND_MODULENAME
|
||||
GetModuleFileNameA(NULL, s, sizeof(s));
|
||||
|
||||
// Log EXE filename only if possible (not full EXE path which could contain sensitive info)
|
||||
AZStd::string exeName;
|
||||
if (AZ::StringFunc::Path::GetFullFileName(s, exeName))
|
||||
{
|
||||
azstrcat(str, length, "Executable: ");
|
||||
azstrcat(str, length, exeName.c_str());
|
||||
|
||||
# ifdef AZ_DEBUG_BUILD
|
||||
azstrcat(str, length, " (debug: yes");
|
||||
# else
|
||||
azstrcat(str, length, " (debug: no");
|
||||
# endif
|
||||
}
|
||||
#endif
|
||||
AZ_POP_DISABLE_WARNING
|
||||
}
|
||||
|
||||
|
||||
//Crash the application, in this way the debug callstack routine will be called and it will create all the necessary files (error.log, dump, and eventually screenshot)
|
||||
void IDebugCallStack::FatalError(const char* description)
|
||||
{
|
||||
m_bIsFatalError = true;
|
||||
WriteLineToLog(description);
|
||||
|
||||
#ifndef _RELEASE
|
||||
bool bShowDebugScreen = g_cvars.sys_no_crash_dialog == 0;
|
||||
// showing the debug screen is not safe when not called from mainthread
|
||||
// it normally leads to a infinity recursion followed by a stack overflow, preventing
|
||||
// useful call stacks, thus they are disabled
|
||||
bShowDebugScreen = bShowDebugScreen && gEnv->mMainThreadId == CryGetCurrentThreadId();
|
||||
if (bShowDebugScreen)
|
||||
{
|
||||
EBUS_EVENT(AZ::NativeUI::NativeUIRequestBus, DisplayOkDialog, "Open 3D Engine Fatal Error", description, false);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(WIN32) || !defined(_RELEASE)
|
||||
int* p = 0x0;
|
||||
PREFAST_SUPPRESS_WARNING(6011) * p = 1; // we're intentionally crashing here
|
||||
#endif
|
||||
}
|
||||
|
||||
void IDebugCallStack::WriteLineToLog(const char* format, ...)
|
||||
{
|
||||
va_list ArgList;
|
||||
char szBuffer[MAX_WARNING_LENGTH];
|
||||
va_start(ArgList, format);
|
||||
vsnprintf_s(szBuffer, sizeof(szBuffer), sizeof(szBuffer) - 1, format, ArgList);
|
||||
cry_strcat(szBuffer, "\n");
|
||||
szBuffer[sizeof(szBuffer) - 1] = '\0';
|
||||
va_end(ArgList);
|
||||
|
||||
AZ::IO::HandleType fileHandle = AZ::IO::InvalidHandle;
|
||||
AZ::IO::FileIOBase::GetDirectInstance()->Open("@Log@\\error.log", AZ::IO::GetOpenModeFromStringMode("a+t"), fileHandle);
|
||||
if (fileHandle != AZ::IO::InvalidHandle)
|
||||
{
|
||||
AZ::IO::FileIOBase::GetDirectInstance()->Write(fileHandle, szBuffer, strlen(szBuffer));
|
||||
AZ::IO::FileIOBase::GetDirectInstance()->Flush(fileHandle);
|
||||
AZ::IO::FileIOBase::GetDirectInstance()->Close(fileHandle);
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
void IDebugCallStack::StartMemLog()
|
||||
{
|
||||
AZ::IO::FileIOBase::GetDirectInstance()->Open("@Log@\\memallocfile.log", AZ::IO::OpenMode::ModeWrite, m_memAllocFileHandle);
|
||||
|
||||
assert(m_memAllocFileHandle != AZ::IO::InvalidHandle);
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
void IDebugCallStack::StopMemLog()
|
||||
{
|
||||
if (m_memAllocFileHandle != AZ::IO::InvalidHandle)
|
||||
{
|
||||
AZ::IO::FileIOBase::GetDirectInstance()->Close(m_memAllocFileHandle);
|
||||
m_memAllocFileHandle = AZ::IO::InvalidHandle;
|
||||
}
|
||||
}
|
||||
//#endif //!defined(LINUX)
|
||||
@@ -0,0 +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.
|
||||
*
|
||||
*/
|
||||
// Original file Copyright Crytek GMBH or its affiliates, used under license.
|
||||
|
||||
// Description : A multiplatform base class for handling errors and collecting call stacks
|
||||
|
||||
#ifndef CRYINCLUDE_CRYSYSTEM_IDEBUGCALLSTACK_H
|
||||
#define CRYINCLUDE_CRYSYSTEM_IDEBUGCALLSTACK_H
|
||||
#pragma once
|
||||
|
||||
#include "System.h"
|
||||
|
||||
#if AZ_LEGACY_CRYSYSTEM_TRAIT_FORWARD_EXCEPTION_POINTERS
|
||||
struct EXCEPTION_POINTERS;
|
||||
#endif
|
||||
//! Limits the maximal number of functions in call stack.
|
||||
enum
|
||||
{
|
||||
MAX_DEBUG_STACK_ENTRIES = 80
|
||||
};
|
||||
|
||||
class IDebugCallStack
|
||||
{
|
||||
public:
|
||||
// Returns single instance of DebugStack
|
||||
static IDebugCallStack* instance();
|
||||
|
||||
virtual int handleException([[maybe_unused]] EXCEPTION_POINTERS* exception_pointer){return 0; }
|
||||
|
||||
// returns the module name of a given address
|
||||
virtual string GetModuleNameForAddr([[maybe_unused]] void* addr) { return "[unknown]"; }
|
||||
|
||||
// returns the function name of a given address together with source file and line number (if available) of a given address
|
||||
virtual void GetProcNameForAddr(void* addr, string& procName, void*& baseAddr, string& filename, int& line)
|
||||
{
|
||||
filename = "[unknown]";
|
||||
line = 0;
|
||||
baseAddr = addr;
|
||||
#if defined(PLATFORM_64BIT)
|
||||
procName.Format("[%016llX]", addr);
|
||||
#else
|
||||
procName.Format("[%08X]", addr);
|
||||
#endif
|
||||
}
|
||||
|
||||
// returns current filename
|
||||
virtual string GetCurrentFilename() { return "[unknown]"; }
|
||||
|
||||
//! Dumps Current Call Stack to log.
|
||||
virtual void LogCallstack();
|
||||
//triggers a fatal error, so the DebugCallstack can create the error.log and terminate the application
|
||||
void FatalError(const char*);
|
||||
|
||||
//Reports a bug and continues execution
|
||||
virtual void ReportBug(const char*) {}
|
||||
|
||||
virtual void FileCreationCallback(void (* postBackupProcess)());
|
||||
|
||||
static void WriteLineToLog(const char* format, ...);
|
||||
|
||||
virtual void StartMemLog();
|
||||
virtual void StopMemLog();
|
||||
|
||||
protected:
|
||||
IDebugCallStack();
|
||||
virtual ~IDebugCallStack();
|
||||
|
||||
static const char* TranslateExceptionCode(DWORD dwExcept);
|
||||
static void PutVersion(char* str, size_t length);
|
||||
|
||||
bool m_bIsFatalError;
|
||||
static const char* const s_szFatalErrorCode;
|
||||
|
||||
void (* m_postBackupProcess)();
|
||||
|
||||
AZ::IO::HandleType m_memAllocFileHandle;
|
||||
};
|
||||
|
||||
|
||||
|
||||
#endif // CRYINCLUDE_CRYSYSTEM_IDEBUGCALLSTACK_H
|
||||
@@ -208,6 +208,7 @@ struct SSystemCVars
|
||||
int sys_no_crash_dialog;
|
||||
int sys_no_error_report_window;
|
||||
int sys_dump_aux_threads;
|
||||
int sys_WER;
|
||||
int sys_dump_type;
|
||||
int sys_ai;
|
||||
int sys_entitysystem;
|
||||
|
||||
@@ -121,6 +121,10 @@
|
||||
# include <AzFramework/Network/AssetProcessorConnection.h>
|
||||
#endif
|
||||
|
||||
#ifdef WIN32
|
||||
extern LONG WINAPI CryEngineExceptionFilterWER(struct _EXCEPTION_POINTERS* pExceptionPointers);
|
||||
#endif
|
||||
|
||||
#if defined(AZ_RESTRICTED_PLATFORM)
|
||||
#define AZ_RESTRICTED_SECTION SYSTEMINIT_CPP_SECTION_14
|
||||
#include AZ_RESTRICTED_FILE(SystemInit_cpp)
|
||||
@@ -1484,6 +1488,13 @@ AZ_POP_DISABLE_WARNING
|
||||
|
||||
InlineInitializationProcessing("CSystem::Init LoadConfigurations");
|
||||
|
||||
#ifdef WIN32
|
||||
if (g_cvars.sys_WER)
|
||||
{
|
||||
SetUnhandledExceptionFilter(CryEngineExceptionFilterWER);
|
||||
}
|
||||
#endif
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// Localization
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
@@ -2020,6 +2031,14 @@ void CSystem::CreateSystemVars()
|
||||
REGISTER_CVAR2("sys_update_profile_time", &g_cvars.sys_update_profile_time, 1.0f, 0, "Time to keep updates timings history for.");
|
||||
REGISTER_CVAR2("sys_no_crash_dialog", &g_cvars.sys_no_crash_dialog, m_bNoCrashDialog, VF_NULL, "Whether to disable the crash dialog window");
|
||||
REGISTER_CVAR2("sys_no_error_report_window", &g_cvars.sys_no_error_report_window, m_bNoErrorReportWindow, VF_NULL, "Whether to disable the error report list");
|
||||
#if defined(_RELEASE)
|
||||
if (!gEnv->IsDedicated())
|
||||
{
|
||||
REGISTER_CVAR2("sys_WER", &g_cvars.sys_WER, 1, 0, "Enables Windows Error Reporting");
|
||||
}
|
||||
#else
|
||||
REGISTER_CVAR2("sys_WER", &g_cvars.sys_WER, 0, 0, "Enables Windows Error Reporting");
|
||||
#endif
|
||||
|
||||
#ifdef USE_HTTP_WEBSOCKETS
|
||||
REGISTER_CVAR2("sys_simple_http_base_port", &g_cvars.sys_simple_http_base_port, 1880, VF_REQUIRE_APP_RESTART,
|
||||
|
||||
@@ -46,6 +46,8 @@
|
||||
#include <shlobj.h>
|
||||
#endif
|
||||
|
||||
#include "IDebugCallStack.h"
|
||||
|
||||
#if defined(APPLE) || defined(LINUX)
|
||||
#include <pwd.h>
|
||||
#endif
|
||||
@@ -355,6 +357,7 @@ void CSystem::FatalError(const char* format, ...)
|
||||
}
|
||||
|
||||
// Dump callstack.
|
||||
IDebugCallStack::instance()->FatalError(szBuffer);
|
||||
#endif
|
||||
|
||||
CryDebugBreak();
|
||||
@@ -396,6 +399,8 @@ void CSystem::ReportBug([[maybe_unused]] const char* format, ...)
|
||||
va_start(ArgList, format);
|
||||
azvsnprintf(szBuffer + strlen(sPrefix), MAX_WARNING_LENGTH - strlen(sPrefix), format, ArgList);
|
||||
va_end(ArgList);
|
||||
|
||||
IDebugCallStack::instance()->ReportBug(szBuffer);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
// Original file Copyright Crytek GMBH or its affiliates, used under license.
|
||||
|
||||
// Description : Support for Windows Error Reporting (WER)
|
||||
|
||||
|
||||
#include "CrySystem_precompiled.h"
|
||||
|
||||
#ifdef WIN32
|
||||
|
||||
#include "System.h"
|
||||
#include <windows.h>
|
||||
#include <tchar.h>
|
||||
#include "errorrep.h"
|
||||
#include "ISystem.h"
|
||||
|
||||
#include <DbgHelp.h>
|
||||
|
||||
static WCHAR szPath[MAX_PATH + 1];
|
||||
static WCHAR szFR[] = L"\\System32\\FaultRep.dll";
|
||||
|
||||
WCHAR* GetFullPathToFaultrepDll(void)
|
||||
{
|
||||
UINT rc = GetSystemWindowsDirectoryW(szPath, ARRAYSIZE(szPath));
|
||||
if (rc == 0 || rc > ARRAYSIZE(szPath) - ARRAYSIZE(szFR) - 1)
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
wcscat_s(szPath, szFR);
|
||||
return szPath;
|
||||
}
|
||||
|
||||
|
||||
typedef BOOL (WINAPI * MINIDUMPWRITEDUMP)(HANDLE hProcess, DWORD dwPid, HANDLE hFile, MINIDUMP_TYPE DumpType,
|
||||
CONST PMINIDUMP_EXCEPTION_INFORMATION ExceptionParam,
|
||||
CONST PMINIDUMP_USER_STREAM_INFORMATION UserStreamParam,
|
||||
CONST PMINIDUMP_CALLBACK_INFORMATION CallbackParam
|
||||
);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
LONG WINAPI CryEngineExceptionFilterMiniDump(struct _EXCEPTION_POINTERS* pExceptionPointers, const char* szDumpPath, MINIDUMP_TYPE DumpType)
|
||||
{
|
||||
// note: In debug mode, this dll is loaded on startup anyway, so this should not incur an additional load unless it crashes
|
||||
// very early during startup.
|
||||
|
||||
fflush(nullptr); // according to MSDN on fflush, calling fflush on null flushes all buffers.
|
||||
HMODULE hndDBGHelpDLL = LoadLibraryA("DBGHELP.DLL");
|
||||
|
||||
if (!hndDBGHelpDLL)
|
||||
{
|
||||
CryLogAlways("Failed to record DMP file: Could not open DBGHELP.DLL");
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
|
||||
MINIDUMPWRITEDUMP dumpFnPtr = (MINIDUMPWRITEDUMP)::GetProcAddress(hndDBGHelpDLL, "MiniDumpWriteDump");
|
||||
if (!dumpFnPtr)
|
||||
{
|
||||
CryLogAlways("Failed to record DMP file: Unable to find MiniDumpWriteDump in DBGHELP.DLL");
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
|
||||
HANDLE hFile = ::CreateFile(szDumpPath, GENERIC_WRITE, FILE_SHARE_WRITE, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
|
||||
if (hFile == INVALID_HANDLE_VALUE)
|
||||
{
|
||||
CryLogAlways("Failed to record DMP file: could not open file '%s' for writing - error code: %d", szDumpPath, GetLastError());
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
|
||||
_MINIDUMP_EXCEPTION_INFORMATION ExInfo;
|
||||
ExInfo.ThreadId = ::GetCurrentThreadId();
|
||||
ExInfo.ExceptionPointers = pExceptionPointers;
|
||||
ExInfo.ClientPointers = NULL;
|
||||
|
||||
BOOL bOK = dumpFnPtr(GetCurrentProcess(), GetCurrentProcessId(), hFile, DumpType, &ExInfo, NULL, NULL);
|
||||
::CloseHandle(hFile);
|
||||
|
||||
if (bOK)
|
||||
{
|
||||
CryLogAlways("Successfully recorded DMP file: '%s'", szDumpPath);
|
||||
return EXCEPTION_EXECUTE_HANDLER; // SUCCESS! you can execute your handlers now
|
||||
}
|
||||
else
|
||||
{
|
||||
CryLogAlways("Failed to record DMP file: '%s' - error code: %d", szDumpPath, GetLastError());
|
||||
}
|
||||
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
LONG WINAPI CryEngineExceptionFilterWER(struct _EXCEPTION_POINTERS* pExceptionPointers)
|
||||
{
|
||||
if (g_cvars.sys_WER > 1)
|
||||
{
|
||||
char szScratch [_MAX_PATH];
|
||||
const char* szDumpPath = gEnv->pCryPak->AdjustFileName("@log@/CE2Dump.dmp", szScratch, AZ_ARRAY_SIZE(szScratch), 0);
|
||||
|
||||
MINIDUMP_TYPE mdumpValue = (MINIDUMP_TYPE)(MiniDumpNormal);
|
||||
if (g_cvars.sys_WER > 1)
|
||||
{
|
||||
mdumpValue = (MINIDUMP_TYPE)(g_cvars.sys_WER - 2);
|
||||
}
|
||||
|
||||
return CryEngineExceptionFilterMiniDump(pExceptionPointers, szDumpPath, mdumpValue);
|
||||
}
|
||||
|
||||
LONG lRet = EXCEPTION_CONTINUE_SEARCH;
|
||||
WCHAR* psz = GetFullPathToFaultrepDll();
|
||||
if (psz)
|
||||
{
|
||||
HMODULE hFaultRepDll = LoadLibraryW(psz);
|
||||
if (hFaultRepDll)
|
||||
{
|
||||
pfn_REPORTFAULT pfn = (pfn_REPORTFAULT)GetProcAddress(hFaultRepDll, "ReportFault");
|
||||
if (pfn)
|
||||
{
|
||||
pfn(pExceptionPointers, 0);
|
||||
lRet = EXCEPTION_EXECUTE_HANDLER;
|
||||
}
|
||||
FreeLibrary(hFaultRepDll);
|
||||
}
|
||||
}
|
||||
return lRet;
|
||||
}
|
||||
|
||||
#endif // WIN32
|
||||
@@ -15,6 +15,8 @@ set(FILES
|
||||
CmdLineArg.cpp
|
||||
ConsoleBatchFile.cpp
|
||||
ConsoleHelpGen.cpp
|
||||
DebugCallStack.cpp
|
||||
IDebugCallStack.cpp
|
||||
Log.cpp
|
||||
System.cpp
|
||||
SystemCFG.cpp
|
||||
@@ -31,6 +33,8 @@ set(FILES
|
||||
CmdLineArg.h
|
||||
ConsoleBatchFile.h
|
||||
ConsoleHelpGen.h
|
||||
DebugCallStack.h
|
||||
IDebugCallStack.h
|
||||
Log.h
|
||||
SimpleStringPool.h
|
||||
CrySystem_precompiled.h
|
||||
@@ -72,4 +76,5 @@ set(FILES
|
||||
ViewSystem/ViewSystem.cpp
|
||||
ViewSystem/ViewSystem.h
|
||||
CrySystem_precompiled.cpp
|
||||
WindowsErrorReporting.cpp
|
||||
)
|
||||
|
||||
@@ -307,6 +307,8 @@ namespace AZ
|
||||
Asset(AssetLoadBehavior loadBehavior = AssetLoadBehavior::Default);
|
||||
/// Create an asset from a valid asset data (created asset), might not be loaded or currently loading.
|
||||
Asset(AssetData* assetData, AssetLoadBehavior loadBehavior);
|
||||
/// Create an asset from a valid asset data (created asset) and set the asset id for both, might not be loaded or currently loading.
|
||||
Asset(const AZ::Data::AssetId& id, AssetData* assetData, AssetLoadBehavior loadBehavior);
|
||||
/// Initialize asset pointer with id, type, and hint. No data construction will occur until QueueLoad is called.
|
||||
Asset(const AZ::Data::AssetId& id, const AZ::Data::AssetType& type, const AZStd::string& hint = AZStd::string());
|
||||
|
||||
@@ -787,6 +789,18 @@ namespace AZ
|
||||
SetData(assetData);
|
||||
}
|
||||
|
||||
//=========================================================================
|
||||
template<class T>
|
||||
Asset<T>::Asset(const AssetId& id, AssetData* assetData, AssetLoadBehavior loadBehavior)
|
||||
: m_assetId(id)
|
||||
, m_assetType(azrtti_typeid<T>())
|
||||
, m_loadBehavior(loadBehavior)
|
||||
{
|
||||
AZ_Assert(!assetData->m_assetId.IsValid(), "Asset data already has an ID set.");
|
||||
assetData->m_assetId = id;
|
||||
SetData(assetData);
|
||||
}
|
||||
|
||||
//=========================================================================
|
||||
template<class T>
|
||||
Asset<T>::Asset(const AssetId& id, const AZ::Data::AssetType& type, const AZStd::string& hint)
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ namespace AzPhysics
|
||||
namespace
|
||||
{
|
||||
const float TimestepMin = 0.001f; //1000fps
|
||||
const float TimestepMax = 0.05f; //20fps
|
||||
const float TimestepMax = 0.1f; //10fps
|
||||
}
|
||||
|
||||
AZ_CLASS_ALLOCATOR_IMPL(SystemConfiguration, AZ::SystemAllocator, 0);
|
||||
|
||||
@@ -34,7 +34,7 @@ namespace AzPhysics
|
||||
|
||||
static constexpr float DefaultFixedTimestep = 0.0166667f; //! Value represents 1/60th or 60 FPS.
|
||||
|
||||
float m_maxTimestep = 1.f / 20.f; //!< Maximum fixed timestep in seconds to run the physics update.
|
||||
float m_maxTimestep = 0.1f; //!< Maximum fixed timestep in seconds to run the physics update (10FPS).
|
||||
float m_fixedTimestep = DefaultFixedTimestep; //!< Timestep in seconds to run the physics update. See DefaultFixedTimestep.
|
||||
|
||||
AZ::u64 m_raycastBufferSize = 32; //!< Maximum number of hits that will be returned from a raycast.
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AzCore/std/string/string.h>
|
||||
|
||||
namespace AzFramework
|
||||
{
|
||||
//! SessionConnectionConfig
|
||||
//! The properties for handling join session request.
|
||||
struct SessionConnectionConfig
|
||||
{
|
||||
// A unique identifier for registered player in session.
|
||||
AZStd::string m_playerSessionId;
|
||||
|
||||
// The DNS identifier assigned to the instance that is running the session.
|
||||
AZStd::string m_dnsName;
|
||||
|
||||
// The IP address of the session.
|
||||
AZStd::string m_ipAddress;
|
||||
|
||||
// The port number for the session.
|
||||
uint16_t m_port;
|
||||
};
|
||||
|
||||
//! SessionConnectionConfig
|
||||
//! The properties for handling player connect/disconnect
|
||||
struct PlayerConnectionConfig
|
||||
{
|
||||
// A unique identifier for player connection.
|
||||
uint32_t m_playerConnectionId;
|
||||
|
||||
// A unique identifier for registered player in session.
|
||||
AZStd::string m_playerSessionId;
|
||||
};
|
||||
|
||||
//! ISessionHandlingClientRequests
|
||||
//! The session handling events to invoke multiplayer component handle the work on client side
|
||||
class ISessionHandlingClientRequests
|
||||
{
|
||||
public:
|
||||
// Handle the player join session process
|
||||
// @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;
|
||||
|
||||
// Handle the player leave session process
|
||||
virtual void HandlePlayerLeaveSession() = 0;
|
||||
};
|
||||
|
||||
//! ISessionHandlingServerRequests
|
||||
//! The session handling events to invoke server provider handle the work on server side
|
||||
class ISessionHandlingServerRequests
|
||||
{
|
||||
public:
|
||||
// Handle the destroy session process
|
||||
virtual void HandleDestroySession() = 0;
|
||||
|
||||
// Validate the player join session process
|
||||
// @param playerConnectionConfig The required properties to validate the player join session process
|
||||
// @return The result of player join session validation
|
||||
virtual bool ValidatePlayerJoinSession(const PlayerConnectionConfig& playerConnectionConfig) = 0;
|
||||
|
||||
// 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;
|
||||
};
|
||||
} // namespace AzFramework
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* 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 <AzCore/Serialization/EditContext.h>
|
||||
#include <AzCore/Serialization/SerializeContext.h>
|
||||
#include <AzFramework/Session/ISessionRequests.h>
|
||||
#include <AzFramework/Session/SessionConfig.h>
|
||||
|
||||
namespace AzFramework
|
||||
{
|
||||
void CreateSessionRequest::Reflect(AZ::ReflectContext* context)
|
||||
{
|
||||
if (auto serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
|
||||
{
|
||||
serializeContext->Class<CreateSessionRequest>()
|
||||
->Version(0)
|
||||
->Field("creatorId", &CreateSessionRequest::m_creatorId)
|
||||
->Field("sessionProperties", &CreateSessionRequest::m_sessionProperties)
|
||||
->Field("sessionName", &CreateSessionRequest::m_sessionName)
|
||||
->Field("maxPlayer", &CreateSessionRequest::m_maxPlayer)
|
||||
;
|
||||
|
||||
if (AZ::EditContext* editContext = serializeContext->GetEditContext())
|
||||
{
|
||||
editContext->Class<CreateSessionRequest>("CreateSessionRequest", "The container for CreateSession request parameters")
|
||||
->ClassElement(AZ::Edit::ClassElements::EditorData, "")
|
||||
->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly)
|
||||
->DataElement(AZ::Edit::UIHandlers::Default, &CreateSessionRequest::m_creatorId,
|
||||
"CreatorId", "A unique identifier for a player or entity creating the session")
|
||||
->DataElement(AZ::Edit::UIHandlers::Default, &CreateSessionRequest::m_sessionProperties,
|
||||
"SessionProperties", "A collection of custom properties for a session")
|
||||
->DataElement(AZ::Edit::UIHandlers::Default, &CreateSessionRequest::m_sessionName,
|
||||
"SessionName", "A descriptive label that is associated with a session")
|
||||
->DataElement(AZ::Edit::UIHandlers::Default, &CreateSessionRequest::m_maxPlayer,
|
||||
"MaxPlayer", "The maximum number of players that can be connected simultaneously to the session")
|
||||
;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SearchSessionsRequest::Reflect(AZ::ReflectContext* context)
|
||||
{
|
||||
if (auto serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
|
||||
{
|
||||
serializeContext->Class<SearchSessionsRequest>()
|
||||
->Version(0)
|
||||
->Field("filterExpression", &SearchSessionsRequest::m_filterExpression)
|
||||
->Field("sortExpression", &SearchSessionsRequest::m_sortExpression)
|
||||
->Field("maxResult", &SearchSessionsRequest::m_maxResult)
|
||||
->Field("nextToken", &SearchSessionsRequest::m_nextToken)
|
||||
;
|
||||
|
||||
if (AZ::EditContext* editContext = serializeContext->GetEditContext())
|
||||
{
|
||||
editContext->Class<SearchSessionsRequest>("SearchSessionsRequest", "The container for SearchSessions request parameters")
|
||||
->ClassElement(AZ::Edit::ClassElements::EditorData, "")
|
||||
->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly)
|
||||
->DataElement(AZ::Edit::UIHandlers::Default, &SearchSessionsRequest::m_filterExpression,
|
||||
"FilterExpression", "String containing the search criteria for the session search")
|
||||
->DataElement(AZ::Edit::UIHandlers::Default, &SearchSessionsRequest::m_sortExpression,
|
||||
"SortExpression", "Instructions on how to sort the search results")
|
||||
->DataElement(AZ::Edit::UIHandlers::Default, &SearchSessionsRequest::m_maxResult,
|
||||
"MaxResult", "The maximum number of results to return")
|
||||
->DataElement(AZ::Edit::UIHandlers::Default, &SearchSessionsRequest::m_nextToken,
|
||||
"NextToken", "A token that indicates the start of the next sequential page of results")
|
||||
;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SearchSessionsResponse::Reflect(AZ::ReflectContext* context)
|
||||
{
|
||||
if (auto serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
|
||||
{
|
||||
serializeContext->Class<SearchSessionsResponse>()
|
||||
->Version(0)
|
||||
->Field("sessionConfigs", &SearchSessionsResponse::m_sessionConfigs)
|
||||
->Field("nextToken", &SearchSessionsResponse::m_nextToken)
|
||||
;
|
||||
|
||||
if (AZ::EditContext* editContext = serializeContext->GetEditContext())
|
||||
{
|
||||
editContext->Class<SearchSessionsResponse>("SearchSessionsResponse", "The container for SearchSession request results")
|
||||
->ClassElement(AZ::Edit::ClassElements::EditorData, "")
|
||||
->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly)
|
||||
->DataElement(AZ::Edit::UIHandlers::Default, &SearchSessionsResponse::m_sessionConfigs,
|
||||
"SessionConfigs", "A collection of sessions that match the search criteria and sorted in specific order")
|
||||
->DataElement(AZ::Edit::UIHandlers::Default, &SearchSessionsResponse::m_nextToken,
|
||||
"NextToken", "A token that indicates the start of the next sequential page of results")
|
||||
;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void JoinSessionRequest::Reflect(AZ::ReflectContext* context)
|
||||
{
|
||||
if (auto serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
|
||||
{
|
||||
serializeContext->Class<JoinSessionRequest>()
|
||||
->Version(0)
|
||||
->Field("sessionId", &JoinSessionRequest::m_sessionId)
|
||||
->Field("playerId", &JoinSessionRequest::m_playerId)
|
||||
->Field("playerData", &JoinSessionRequest::m_playerData)
|
||||
;
|
||||
|
||||
if (AZ::EditContext* editContext = serializeContext->GetEditContext())
|
||||
{
|
||||
editContext->Class<JoinSessionRequest>("JoinSessionRequest", "The container for JoinSession request parameters")
|
||||
->ClassElement(AZ::Edit::ClassElements::EditorData, "")
|
||||
->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly)
|
||||
->DataElement(AZ::Edit::UIHandlers::Default, &JoinSessionRequest::m_sessionId,
|
||||
"SessionId", "A unique identifier for the session")
|
||||
->DataElement(AZ::Edit::UIHandlers::Default, &JoinSessionRequest::m_playerId,
|
||||
"PlayerId", "A unique identifier for a player. Player IDs are developer-defined")
|
||||
->DataElement(AZ::Edit::UIHandlers::Default, &JoinSessionRequest::m_playerData,
|
||||
"PlayerData", "Developer-defined information related to a player")
|
||||
;
|
||||
}
|
||||
}
|
||||
}
|
||||
} // namespace AzFramework
|
||||
@@ -0,0 +1,192 @@
|
||||
/*
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AzCore/EBus/EBus.h>
|
||||
#include <AzCore/RTTI/ReflectContext.h>
|
||||
#include <AzCore/std/containers/unordered_map.h>
|
||||
#include <AzCore/std/string/string.h>
|
||||
#include <AzCore/Outcome/Outcome.h>
|
||||
|
||||
namespace AzFramework
|
||||
{
|
||||
struct SessionConfig;
|
||||
|
||||
//! CreateSessionRequest
|
||||
//! The container for CreateSession request parameters.
|
||||
struct CreateSessionRequest
|
||||
{
|
||||
AZ_RTTI(CreateSessionRequest, "{E39C2A45-89C9-4CFB-B337-9734DC798930}");
|
||||
static void Reflect(AZ::ReflectContext* context);
|
||||
|
||||
CreateSessionRequest() = default;
|
||||
virtual ~CreateSessionRequest() = default;
|
||||
|
||||
// A unique identifier for a player or entity creating the session.
|
||||
AZStd::string m_creatorId;
|
||||
|
||||
// A collection of custom properties for a session.
|
||||
AZStd::unordered_map<AZStd::string, AZStd::string> m_sessionProperties;
|
||||
|
||||
// A descriptive label that is associated with a session.
|
||||
AZStd::string m_sessionName;
|
||||
|
||||
// The maximum number of players that can be connected simultaneously to the session.
|
||||
uint64_t m_maxPlayer;
|
||||
};
|
||||
|
||||
//! SearchSessionsRequest
|
||||
//! The container for SearchSessions request parameters.
|
||||
struct SearchSessionsRequest
|
||||
{
|
||||
AZ_RTTI(SearchSessionsRequest, "{B49207A8-8549-4ADB-B7D9-D7A4932F9B4B}");
|
||||
static void Reflect(AZ::ReflectContext* context);
|
||||
|
||||
SearchSessionsRequest() = default;
|
||||
virtual ~SearchSessionsRequest() = default;
|
||||
|
||||
// String containing the search criteria for the session search. If no filter expression is included, the request returns results
|
||||
// for all active sessions.
|
||||
AZStd::string m_filterExpression;
|
||||
|
||||
// Instructions on how to sort the search results. If no sort expression is included, the request returns results in random order.
|
||||
AZStd::string m_sortExpression;
|
||||
|
||||
// The maximum number of results to return.
|
||||
uint8_t m_maxResult;
|
||||
|
||||
// A token that indicates the start of the next sequential page of results.
|
||||
AZStd::string m_nextToken;
|
||||
};
|
||||
|
||||
//! SearchSessionsResponse
|
||||
//! The container for SearchSession request results.
|
||||
struct SearchSessionsResponse
|
||||
{
|
||||
AZ_RTTI(SearchSessionsResponse, "{F93DE7DC-D381-4E08-8A3B-0B08F7C38714}");
|
||||
static void Reflect(AZ::ReflectContext* context);
|
||||
|
||||
SearchSessionsResponse() = default;
|
||||
virtual ~SearchSessionsResponse() = default;
|
||||
|
||||
// A collection of sessions that match the search criteria and sorted in specific order.
|
||||
AZStd::vector<SessionConfig> m_sessionConfigs;
|
||||
|
||||
// A token that indicates the start of the next sequential page of results.
|
||||
AZStd::string m_nextToken;
|
||||
};
|
||||
|
||||
//! JoinSessionRequest
|
||||
//! The container for JoinSession request parameters.
|
||||
struct JoinSessionRequest
|
||||
{
|
||||
AZ_RTTI(JoinSessionRequest, "{519769E8-3CDE-4385-A0D7-24DBB3685657}");
|
||||
static void Reflect(AZ::ReflectContext* context);
|
||||
|
||||
JoinSessionRequest() = default;
|
||||
virtual ~JoinSessionRequest() = default;
|
||||
|
||||
// A unique identifier for the session.
|
||||
AZStd::string m_sessionId;
|
||||
|
||||
// A unique identifier for a player. Player IDs are developer-defined.
|
||||
AZStd::string m_playerId;
|
||||
|
||||
// Developer-defined information related to a player.
|
||||
AZStd::string m_playerData;
|
||||
};
|
||||
|
||||
//! ISessionRequests
|
||||
//! Pure virtual session interface class to abstract the details of session handling from application code.
|
||||
class ISessionRequests
|
||||
{
|
||||
public:
|
||||
AZ_RTTI(ISessionRequests, "{D6C41A71-DD8D-47FE-8515-FAF90670AE2F}");
|
||||
|
||||
ISessionRequests() = default;
|
||||
virtual ~ISessionRequests() = default;
|
||||
|
||||
// Create a session for players to find and join.
|
||||
// @param createSessionRequest The request of CreateSession operation
|
||||
// @return The request id if session creation request succeeds; empty if it fails
|
||||
virtual AZStd::string CreateSession(const CreateSessionRequest& createSessionRequest) = 0;
|
||||
|
||||
// Retrieve all active sessions that match the given search criteria and sorted in specific order.
|
||||
// @param searchSessionsRequest The request of SearchSessions operation
|
||||
// @return The response of SearchSessions operation
|
||||
virtual SearchSessionsResponse SearchSessions(const SearchSessionsRequest& searchSessionsRequest) const = 0;
|
||||
|
||||
// Reserve an open player slot in a session, and perform connection from client to server.
|
||||
// @param joinSessionRequest The request of JoinSession operation
|
||||
// @return True if joining session succeeds; False otherwise
|
||||
virtual bool JoinSession(const JoinSessionRequest& joinSessionRequest) = 0;
|
||||
|
||||
// Disconnect player from session.
|
||||
virtual void LeaveSession() = 0;
|
||||
};
|
||||
|
||||
//! ISessionAsyncRequests
|
||||
//! Async version of ISessionRequests
|
||||
class ISessionAsyncRequests
|
||||
{
|
||||
public:
|
||||
AZ_RTTI(ISessionAsyncRequests, "{471542AF-96B9-4930-82FE-242A4E68432D}");
|
||||
|
||||
ISessionAsyncRequests() = default;
|
||||
virtual ~ISessionAsyncRequests() = default;
|
||||
|
||||
// CreateSession Async
|
||||
// @param createSessionRequest The request of CreateSession operation
|
||||
virtual void CreateSessionAsync(const CreateSessionRequest& createSessionRequest) = 0;
|
||||
|
||||
// SearchSessions Async
|
||||
// @param searchSessionsRequest The request of SearchSessions operation
|
||||
virtual void SearchSessionsAsync(const SearchSessionsRequest& searchSessionsRequest) const = 0;
|
||||
|
||||
// JoinSession Async
|
||||
// @param joinSessionRequest The request of JoinSession operation
|
||||
virtual void JoinSessionAsync(const JoinSessionRequest& joinSessionRequest) = 0;
|
||||
|
||||
// LeaveSession Async
|
||||
virtual void LeaveSessionAsync() = 0;
|
||||
};
|
||||
|
||||
//! SessionAsyncRequestNotifications
|
||||
//! The notifications correspond to session async requests
|
||||
class SessionAsyncRequestNotifications
|
||||
: public AZ::EBusTraits
|
||||
{
|
||||
public:
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// EBusTraits overrides
|
||||
static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Multiple;
|
||||
static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single;
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// OnCreateSessionAsyncComplete is fired once CreateSessionAsync completes
|
||||
// @param createSessionResponse The request id if session creation request succeeds; empty if it fails
|
||||
virtual void OnCreateSessionAsyncComplete(const AZStd::string& createSessionReponse) = 0;
|
||||
|
||||
// OnSearchSessionsAsyncComplete is fired once SearchSessionsAsync completes
|
||||
// @param searchSessionsResponse The response of SearchSessions call
|
||||
virtual void OnSearchSessionsAsyncComplete(const SearchSessionsResponse& searchSessionsResponse) = 0;
|
||||
|
||||
// OnJoinSessionAsyncComplete is fired once JoinSessionAsync completes
|
||||
// @param joinSessionsResponse True if joining session succeeds; False otherwise
|
||||
virtual void OnJoinSessionAsyncComplete(bool joinSessionsResponse) = 0;
|
||||
|
||||
// OnLeaveSessionAsyncComplete is fired once LeaveSessionAsync completes
|
||||
virtual void OnLeaveSessionAsyncComplete() = 0;
|
||||
};
|
||||
using SessionAsyncRequestNotificationBus = AZ::EBus<SessionAsyncRequestNotifications>;
|
||||
} // namespace AzFramework
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* 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 <AzCore/Serialization/EditContext.h>
|
||||
#include <AzCore/Serialization/SerializeContext.h>
|
||||
#include <AzFramework/Session/SessionConfig.h>
|
||||
|
||||
namespace AzFramework
|
||||
{
|
||||
void SessionConfig::Reflect(AZ::ReflectContext* context)
|
||||
{
|
||||
if (auto serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
|
||||
{
|
||||
serializeContext->Class<SessionConfig>()
|
||||
->Version(0)
|
||||
->Field("creationTime", &SessionConfig::m_creationTime)
|
||||
->Field("terminationTime", &SessionConfig::m_terminationTime)
|
||||
->Field("creatorId", &SessionConfig::m_creatorId)
|
||||
->Field("sessionProperties", &SessionConfig::m_sessionProperties)
|
||||
->Field("sessionId", &SessionConfig::m_sessionId)
|
||||
->Field("sessionName", &SessionConfig::m_sessionName)
|
||||
->Field("dnsName", &SessionConfig::m_dnsName)
|
||||
->Field("ipAddress", &SessionConfig::m_ipAddress)
|
||||
->Field("port", &SessionConfig::m_port)
|
||||
->Field("maxPlayer", &SessionConfig::m_maxPlayer)
|
||||
->Field("currentPlayer", &SessionConfig::m_currentPlayer)
|
||||
->Field("status", &SessionConfig::m_status)
|
||||
->Field("statusReason", &SessionConfig::m_statusReason)
|
||||
;
|
||||
|
||||
if (AZ::EditContext* editContext = serializeContext->GetEditContext())
|
||||
{
|
||||
editContext->Class<SessionConfig>("SessionConfig", "Properties describing a session")
|
||||
->ClassElement(AZ::Edit::ClassElements::EditorData, "")
|
||||
->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly)
|
||||
->DataElement(AZ::Edit::UIHandlers::Default, &AzFramework::SessionConfig::m_creationTime,
|
||||
"CreationTime", "A time stamp indicating when this session was created. Format is a number expressed in Unix time as milliseconds.")
|
||||
->DataElement(AZ::Edit::UIHandlers::Default, &AzFramework::SessionConfig::m_terminationTime,
|
||||
"TerminationTime", "A time stamp indicating when this data object was terminated. Same format as creation time.")
|
||||
->DataElement(AZ::Edit::UIHandlers::Default, &AzFramework::SessionConfig::m_creatorId,
|
||||
"CreatorId", "A unique identifier for a player or entity creating the session.")
|
||||
->DataElement(AZ::Edit::UIHandlers::Default, &AzFramework::SessionConfig::m_sessionProperties,
|
||||
"SessionProperties", "A collection of custom properties for a session.")
|
||||
->DataElement(AZ::Edit::UIHandlers::Default, &AzFramework::SessionConfig::m_sessionId,
|
||||
"SessionId", "A unique identifier for the session.")
|
||||
->DataElement(AZ::Edit::UIHandlers::Default, &AzFramework::SessionConfig::m_sessionName,
|
||||
"SessionName", "A descriptive label that is associated with a session.")
|
||||
->DataElement(AZ::Edit::UIHandlers::Default, &AzFramework::SessionConfig::m_dnsName,
|
||||
"DnsName", "The DNS identifier assigned to the instance that is running the session.")
|
||||
->DataElement(AZ::Edit::UIHandlers::Default, &AzFramework::SessionConfig::m_ipAddress,
|
||||
"IpAddress", "The IP address of the session.")
|
||||
->DataElement(AZ::Edit::UIHandlers::Default, &AzFramework::SessionConfig::m_port,
|
||||
"Port", "The port number for the session.")
|
||||
->DataElement(AZ::Edit::UIHandlers::Default, &AzFramework::SessionConfig::m_maxPlayer,
|
||||
"MaxPlayer", "The maximum number of players that can be connected simultaneously to the session.")
|
||||
->DataElement(AZ::Edit::UIHandlers::Default, &AzFramework::SessionConfig::m_currentPlayer,
|
||||
"CurrentPlayer", "Number of players currently in the session.")
|
||||
->DataElement(AZ::Edit::UIHandlers::Default, &AzFramework::SessionConfig::m_status,
|
||||
"Status", "Current status of the session.")
|
||||
->DataElement(AZ::Edit::UIHandlers::Default, &AzFramework::SessionConfig::m_statusReason,
|
||||
"StatusReason", "Provides additional information about session status.");
|
||||
}
|
||||
}
|
||||
}
|
||||
} // namespace AzFramework
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AzCore/RTTI/ReflectContext.h>
|
||||
#include <AzCore/std/containers/unordered_map.h>
|
||||
#include <AzCore/std/string/string.h>
|
||||
|
||||
namespace AzFramework
|
||||
{
|
||||
//! SessionConfig
|
||||
//! Properties describing a session.
|
||||
struct SessionConfig
|
||||
{
|
||||
AZ_RTTI(SessionConfig, "{992DD4BE-8BA5-4071-8818-B99FD2952086}");
|
||||
static void Reflect(AZ::ReflectContext* context);
|
||||
|
||||
SessionConfig() = default;
|
||||
virtual ~SessionConfig() = default;
|
||||
|
||||
// A time stamp indicating when this session was created. Format is a number expressed in Unix time as milliseconds.
|
||||
uint64_t m_creationTime;
|
||||
|
||||
// A time stamp indicating when this data object was terminated. Same format as creation time.
|
||||
uint64_t m_terminationTime;
|
||||
|
||||
// A unique identifier for a player or entity creating the session.
|
||||
AZStd::string m_creatorId;
|
||||
|
||||
// A collection of custom properties for a session.
|
||||
AZStd::unordered_map<AZStd::string, AZStd::string> m_sessionProperties;
|
||||
|
||||
// A unique identifier for the session.
|
||||
AZStd::string m_sessionId;
|
||||
|
||||
// A descriptive label that is associated with a session.
|
||||
AZStd::string m_sessionName;
|
||||
|
||||
// The DNS identifier assigned to the instance that is running the session.
|
||||
AZStd::string m_dnsName;
|
||||
|
||||
// The IP address of the session.
|
||||
AZStd::string m_ipAddress;
|
||||
|
||||
// The port number for the session.
|
||||
uint16_t m_port;
|
||||
|
||||
// The maximum number of players that can be connected simultaneously to the session.
|
||||
uint64_t m_maxPlayer;
|
||||
|
||||
// Number of players currently in the session.
|
||||
uint64_t m_currentPlayer;
|
||||
|
||||
// Current status of the session.
|
||||
AZStd::string m_status;
|
||||
|
||||
// Provides additional information about session status.
|
||||
AZStd::string m_statusReason;
|
||||
};
|
||||
} // namespace AzFramework
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AzCore/EBus/EBus.h>
|
||||
|
||||
namespace AzFramework
|
||||
{
|
||||
struct SessionConfig;
|
||||
|
||||
//! SessionNotifications
|
||||
//! The session notifications to listen for performing required operations
|
||||
class SessionNotifications
|
||||
: public AZ::EBusTraits
|
||||
{
|
||||
public:
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// EBusTraits overrides
|
||||
static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Multiple;
|
||||
static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single;
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// OnSessionHealthCheck is fired in health check process
|
||||
// @return The result of all OnSessionHealthCheck
|
||||
virtual bool OnSessionHealthCheck() = 0;
|
||||
|
||||
// OnCreateSessionBegin is fired at the beginning of session creation
|
||||
// @param sessionConfig The properties to describe a session
|
||||
// @return The result of all OnCreateSessionBegin notifications
|
||||
virtual bool OnCreateSessionBegin(const SessionConfig& sessionConfig) = 0;
|
||||
|
||||
// OnDestroySessionBegin is fired at the beginning of session termination
|
||||
// @return The result of all OnDestroySessionBegin notifications
|
||||
virtual bool OnDestroySessionBegin() = 0;
|
||||
};
|
||||
using SessionNotificationBus = AZ::EBus<SessionNotifications>;
|
||||
} // namespace AzFramework
|
||||
@@ -11,6 +11,7 @@
|
||||
*/
|
||||
|
||||
#include <AzCore/Component/ComponentApplicationBus.h>
|
||||
#include <AzCore/Serialization/IdUtils.h>
|
||||
#include <AzCore/Serialization/SerializeContext.h>
|
||||
#include <AzCore/std/parallel/scoped_lock.h>
|
||||
#include <AzCore/std/smart_ptr/make_shared.h>
|
||||
@@ -215,6 +216,13 @@ namespace AzFramework
|
||||
return clone;
|
||||
}
|
||||
|
||||
AZ::Entity* SpawnableEntitiesManager::CloneSingleEntity(const AZ::Entity& entityTemplate,
|
||||
EntityIdMap& templateToCloneEntityIdMap, AZ::SerializeContext& serializeContext)
|
||||
{
|
||||
return AZ::IdUtils::Remapper<AZ::EntityId>::CloneObjectAndGenerateNewIdsAndFixRefs(
|
||||
&entityTemplate, templateToCloneEntityIdMap, &serializeContext);
|
||||
}
|
||||
|
||||
bool SpawnableEntitiesManager::ProcessRequest(SpawnAllEntitiesCommand& request, AZ::SerializeContext& serializeContext)
|
||||
{
|
||||
Ticket& ticket = GetTicketPayload<Ticket>(*request.m_ticket);
|
||||
@@ -230,48 +238,37 @@ namespace AzFramework
|
||||
const Spawnable::EntityList& entitiesToSpawn = ticket.m_spawnable->GetEntities();
|
||||
size_t entitiesToSpawnSize = entitiesToSpawn.size();
|
||||
|
||||
// Map keeps track of ids from template (spawnable) to clone (instance)
|
||||
// Allowing patch ups of fields referring to entityIds outside of a given entity
|
||||
EntityIdMap templateToCloneEntityIdMap;
|
||||
|
||||
// Reserve buffers
|
||||
spawnedEntities.reserve(spawnedEntities.size() + entitiesToSpawnSize);
|
||||
ticket.m_spawnedEntityIndices.reserve(ticket.m_spawnedEntityIndices.size() + entitiesToSpawnSize);
|
||||
spawnedEntityIndices.reserve(spawnedEntityIndices.size() + entitiesToSpawnSize);
|
||||
templateToCloneEntityIdMap.reserve(entitiesToSpawnSize);
|
||||
|
||||
// TEMP: To be replaced by IdUtils::Remapper
|
||||
using EntityIdMap = AZStd::unordered_map<AZ::EntityId, AZ::EntityId>;
|
||||
EntityIdMap templateToCloneIdMap;
|
||||
// \TEMP
|
||||
|
||||
// Clone the entities from Spawnable
|
||||
// Mark all indices as spawned
|
||||
for (size_t i = 0; i < entitiesToSpawnSize; ++i)
|
||||
{
|
||||
const AZ::Entity& entityTemplate = *entitiesToSpawn[i];
|
||||
|
||||
AZ::Entity* clone = serializeContext.CloneObject(&entityTemplate);
|
||||
AZ::Entity* clone = CloneSingleEntity(entityTemplate, templateToCloneEntityIdMap, serializeContext);
|
||||
|
||||
AZ_Assert(clone != nullptr, "Failed to clone spawnable entity.");
|
||||
clone->SetId(AZ::Entity::MakeId());
|
||||
|
||||
spawnedEntities.push_back(clone);
|
||||
spawnedEntities.emplace_back(clone);
|
||||
spawnedEntityIndices.push_back(i);
|
||||
}
|
||||
|
||||
// TEMP: To be replaced by IdUtils::Remapper
|
||||
templateToCloneIdMap[entityTemplate.GetId()] = clone->GetId();
|
||||
|
||||
// Update TransformComponent parent Id. It is guaranteed for the entities array to be sorted from parent->child here.
|
||||
auto* transformComponent = clone->FindComponent<AzFramework::TransformComponent>();
|
||||
AZ::EntityId parentId = transformComponent->GetParentId();
|
||||
if (parentId.IsValid())
|
||||
{
|
||||
auto it = templateToCloneIdMap.find(parentId);
|
||||
if (it != templateToCloneIdMap.end())
|
||||
{
|
||||
transformComponent->SetParentRelative(it->second);
|
||||
}
|
||||
else
|
||||
{
|
||||
AZ_Warning(
|
||||
"SpawnableEntitiesManager", false, "Entity %s doesn't have the parent entity %s present in the spawnable",
|
||||
clone->GetName().c_str(), parentId.ToString().data());
|
||||
}
|
||||
}
|
||||
// \TEMP
|
||||
// loadAll is true if every entity has been spawned only once
|
||||
if (spawnedEntities.size() == entitiesToSpawnSize)
|
||||
{
|
||||
ticket.m_loadAll = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Case where there were already spawns from a previous request
|
||||
ticket.m_loadAll = false;
|
||||
}
|
||||
|
||||
// Let other systems know about newly spawned entities for any pre-processing before adding to the scene/game context.
|
||||
@@ -437,10 +434,23 @@ namespace AzFramework
|
||||
// to load every, simply start over.
|
||||
ticket.m_spawnedEntityIndices.clear();
|
||||
|
||||
size_t entitiesSize = entities.size();
|
||||
for (size_t i = 0; i < entitiesSize; ++i)
|
||||
size_t entitiesToSpawnSize = entities.size();
|
||||
|
||||
// Map keeps track of ids from template (spawnable) to clone (instance)
|
||||
// Allowing patch ups of fields referring to entityIds outside of a given entity
|
||||
EntityIdMap templateToCloneEntityIdMap;
|
||||
templateToCloneEntityIdMap.reserve(entitiesToSpawnSize);
|
||||
|
||||
// Mark all indices as spawned
|
||||
for (size_t i = 0; i < entitiesToSpawnSize; ++i)
|
||||
{
|
||||
ticket.m_spawnedEntities.push_back(SpawnSingleEntity(*entities[i], serializeContext));
|
||||
const AZ::Entity& entityTemplate = *entities[i];
|
||||
|
||||
AZ::Entity* clone = CloneSingleEntity(entityTemplate, templateToCloneEntityIdMap, serializeContext);
|
||||
|
||||
AZ_Assert(clone != nullptr, "Failed to clone spawnable entity.");
|
||||
|
||||
ticket.m_spawnedEntities.emplace_back(clone);
|
||||
ticket.m_spawnedEntityIndices.push_back(i);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,8 @@ namespace AZ
|
||||
|
||||
namespace AzFramework
|
||||
{
|
||||
using EntityIdMap = AZStd::unordered_map<AZ::EntityId, AZ::EntityId>;
|
||||
|
||||
class SpawnableEntitiesManager
|
||||
: public SpawnableEntitiesInterface::Registrar
|
||||
{
|
||||
@@ -142,7 +144,11 @@ namespace AzFramework
|
||||
using Requests = AZStd::variant<SpawnAllEntitiesCommand, SpawnEntitiesCommand, DespawnAllEntitiesCommand, ReloadSpawnableCommand,
|
||||
ListEntitiesCommand, ClaimEntitiesCommand, BarrierCommand, DestroyTicketCommand>;
|
||||
|
||||
AZ::Entity* SpawnSingleEntity(const AZ::Entity& entityTemplate, AZ::SerializeContext& serializeContext);
|
||||
AZ::Entity* SpawnSingleEntity(const AZ::Entity& entityTemplate,
|
||||
AZ::SerializeContext& serializeContext);
|
||||
|
||||
AZ::Entity* CloneSingleEntity(const AZ::Entity& entityTemplate,
|
||||
EntityIdMap& templateToCloneEntityIdMap, AZ::SerializeContext& serializeContext);
|
||||
|
||||
bool ProcessRequest(SpawnAllEntitiesCommand& request, AZ::SerializeContext& serializeContext);
|
||||
bool ProcessRequest(SpawnEntitiesCommand& request, AZ::SerializeContext& serializeContext);
|
||||
|
||||
@@ -188,6 +188,12 @@ set(FILES
|
||||
Script/ScriptDebugMsgReflection.h
|
||||
Script/ScriptRemoteDebugging.cpp
|
||||
Script/ScriptRemoteDebugging.h
|
||||
Session/ISessionHandlingRequests.h
|
||||
Session/ISessionRequests.cpp
|
||||
Session/ISessionRequests.h
|
||||
Session/SessionConfig.cpp
|
||||
Session/SessionConfig.h
|
||||
Session/SessionNotifications.h
|
||||
StreamingInstall/StreamingInstall.h
|
||||
StreamingInstall/StreamingInstall.cpp
|
||||
StreamingInstall/StreamingInstallRequests.h
|
||||
|
||||
@@ -116,8 +116,8 @@ namespace AzNetworking
|
||||
|
||||
int32_t TcpSocket::Receive(uint8_t* outData, uint32_t size) const
|
||||
{
|
||||
AZ_Assert(size > 0, "Invalid data size for send");
|
||||
AZ_Assert(outData != nullptr, "NULL data pointer passed to send");
|
||||
AZ_Assert(size > 0, "Invalid data size for receive");
|
||||
AZ_Assert(outData != nullptr, "NULL data pointer passed to receive");
|
||||
if (!IsOpen())
|
||||
{
|
||||
return SocketOpResultErrorNotOpen;
|
||||
@@ -176,7 +176,7 @@ namespace AzNetworking
|
||||
if (::bind(aznumeric_cast<int32_t>(m_socketFd), (const sockaddr*)&hints, sizeof(hints)) != 0)
|
||||
{
|
||||
const int32_t error = GetLastNetworkError();
|
||||
AZLOG_ERROR("Failed to bind socket (%d:%s)", error, GetNetworkErrorDesc(error));
|
||||
AZLOG_ERROR("Failed to bind TCP socket to port %u (%d:%s)", uint32_t(port), error, GetNetworkErrorDesc(error));
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -162,7 +162,7 @@ namespace AzNetworking
|
||||
const UdpReaderThread::ReceivedPackets* packets = m_readerThread.GetReceivedPackets(m_socket.get());
|
||||
if (packets == nullptr)
|
||||
{
|
||||
AZ_Assert(false, "nullptr was retrieved for the received packet buffer, check that the socket has been registered with the reader thread");
|
||||
// Socket is not yet registered with the reader thread and is likely still pending, try again later
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ namespace AzNetworking
|
||||
if (::bind(static_cast<int32_t>(m_socketFd), (const sockaddr *)&hints, sizeof(hints)) != 0)
|
||||
{
|
||||
const int32_t error = GetLastNetworkError();
|
||||
AZLOG_ERROR("Failed to bind socket to port %u (%d:%s)", uint32_t(port), error, GetNetworkErrorDesc(error));
|
||||
AZLOG_ERROR("Failed to bind UDP socket to port %u (%d:%s)", uint32_t(port), error, GetNetworkErrorDesc(error));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
+5
@@ -12,6 +12,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AzCore/Asset/AssetCommon.h>
|
||||
#include <AzCore/Component/Entity.h>
|
||||
#include <AzCore/IO/GenericStreams.h>
|
||||
#include <AzCore/std/smart_ptr/unique_ptr.h>
|
||||
@@ -46,6 +47,10 @@ namespace AzToolsFramework
|
||||
|
||||
virtual Prefab::InstanceOptionalReference GetRootPrefabInstance() = 0;
|
||||
|
||||
//! Get all Assets generated by Prefab processing when entering Play-In Editor mode (Ctrl+G)
|
||||
//! /return The vector of Assets generated by Prefab processing
|
||||
virtual const AZStd::vector<AZ::Data::Asset<AZ::Data::AssetData>>& GetPlayInEditorAssetData() = 0;
|
||||
|
||||
virtual bool LoadFromStream(AZ::IO::GenericStream& stream, AZStd::string_view filename) = 0;
|
||||
virtual bool SaveToStream(AZ::IO::GenericStream& stream, AZStd::string_view filename) = 0;
|
||||
|
||||
|
||||
+5
@@ -314,6 +314,11 @@ namespace AzToolsFramework
|
||||
return *m_rootInstance;
|
||||
}
|
||||
|
||||
const AZStd::vector<AZ::Data::Asset<AZ::Data::AssetData>>& PrefabEditorEntityOwnershipService::GetPlayInEditorAssetData()
|
||||
{
|
||||
return m_playInEditorData.m_assets;
|
||||
}
|
||||
|
||||
void PrefabEditorEntityOwnershipService::OnEntityRemoved(AZ::EntityId entityId)
|
||||
{
|
||||
AzFramework::SliceEntityRequestBus::MultiHandler::BusDisconnect(entityId);
|
||||
|
||||
+2
@@ -195,6 +195,8 @@ namespace AzToolsFramework
|
||||
AZ::IO::PathView filePath, Prefab::InstanceOptionalReference instanceToParentUnder) override;
|
||||
|
||||
Prefab::InstanceOptionalReference GetRootPrefabInstance() override;
|
||||
|
||||
const AZStd::vector<AZ::Data::Asset<AZ::Data::AssetData>>& GetPlayInEditorAssetData() override;
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
void OnEntityRemoved(AZ::EntityId entityId);
|
||||
|
||||
+28
@@ -27,6 +27,7 @@ AZ_PUSH_DISABLE_WARNING(4244 4251 4800, "-Wunknown-warning-option") // 4244: con
|
||||
#include <QtGui/QTextLayout>
|
||||
#include <QtGui/QPainter>
|
||||
#include <QMessageBox>
|
||||
#include <QStylePainter>
|
||||
AZ_POP_DISABLE_WARNING
|
||||
|
||||
static const int LabelColumnStretch = 2;
|
||||
@@ -121,6 +122,19 @@ namespace AzToolsFramework
|
||||
setLayout(m_mainLayout);
|
||||
}
|
||||
|
||||
void PropertyRowWidget::paintEvent(QPaintEvent* event)
|
||||
{
|
||||
QStylePainter p(this);
|
||||
|
||||
if (CanBeReordered())
|
||||
{
|
||||
const QPen linePen(QColor(0x3B3E3F));
|
||||
p.setPen(linePen);
|
||||
int indent = m_treeDepth * m_treeIndentation;
|
||||
p.drawLine(event->rect().topLeft() + QPoint(indent, 0), event->rect().topRight());
|
||||
}
|
||||
}
|
||||
|
||||
bool PropertyRowWidget::HasChildWidgetAlready() const
|
||||
{
|
||||
return m_childWidget != nullptr;
|
||||
@@ -1661,6 +1675,20 @@ namespace AzToolsFramework
|
||||
m_nameLabel->setFilter(m_currentFilterString);
|
||||
}
|
||||
|
||||
bool PropertyRowWidget::CanChildrenBeReordered() const
|
||||
{
|
||||
return m_containerEditable;
|
||||
}
|
||||
|
||||
bool PropertyRowWidget::CanBeReordered() const
|
||||
{
|
||||
if (!m_parentRow)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return m_parentRow->CanChildrenBeReordered();
|
||||
}
|
||||
}
|
||||
|
||||
#include "UI/PropertyEditor/moc_PropertyRowWidget.cpp"
|
||||
|
||||
+5
@@ -44,6 +44,7 @@ namespace AzToolsFramework
|
||||
Q_PROPERTY(bool hasChildRows READ HasChildRows);
|
||||
Q_PROPERTY(bool isTopLevel READ IsTopLevel);
|
||||
Q_PROPERTY(int getLevel READ GetLevel);
|
||||
Q_PROPERTY(bool canBeReordered READ CanBeReordered);
|
||||
Q_PROPERTY(bool appendDefaultLabelToName READ GetAppendDefaultLabelToName WRITE AppendDefaultLabelToName)
|
||||
public:
|
||||
AZ_CLASS_ALLOCATOR(PropertyRowWidget, AZ::SystemAllocator, 0)
|
||||
@@ -126,6 +127,7 @@ namespace AzToolsFramework
|
||||
void SetSelectionEnabled(bool selectionEnabled);
|
||||
void SetSelected(bool selected);
|
||||
bool eventFilter(QObject *watched, QEvent *event) override;
|
||||
void paintEvent(QPaintEvent*) override;
|
||||
|
||||
/// Apply tooltip to widget and some of its children.
|
||||
void SetDescription(const QString& text);
|
||||
@@ -146,6 +148,9 @@ namespace AzToolsFramework
|
||||
QLabel* GetNameLabel() { return m_nameLabel; }
|
||||
void SetIndentSize(int w);
|
||||
void SetAsCustom(bool custom) { m_custom = custom; }
|
||||
|
||||
bool CanChildrenBeReordered() const;
|
||||
bool CanBeReordered() const;
|
||||
protected:
|
||||
int CalculateLabelWidth() const;
|
||||
|
||||
|
||||
@@ -38,6 +38,11 @@ AzToolsFramework--ComponentPaletteWidget > QTreeView
|
||||
background-color: #222222;
|
||||
}
|
||||
|
||||
AzToolsFramework--PropertyRowWidget[canBeReordered="true"] QLabel#Name
|
||||
{
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Style for visualizing property values overridden from their prefab values */
|
||||
AzToolsFramework--PropertyRowWidget[IsOverridden=true] #Name QLabel,
|
||||
AzToolsFramework--ComponentEditorHeader #Title[IsOverridden="true"]
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7088e902885d98953f6a1715efab319c063a4ab8918fd0e810251c8ed82b8514
|
||||
size 542983
|
||||
@@ -0,0 +1,4 @@
|
||||
SPDX-FileCopyrightText: Unsplash grants you an irrevocable, nonexclusive, worldwide copyright license to download, copy,
|
||||
SPDX-FileCopyrightText: modify, distribute, perform, and use photos from Unsplash for free, including for commercial
|
||||
SPDX-FileCopyrightText: purposes, without permission from or attributing the photographer or Unsplash. This license does
|
||||
SPDX-FileCopyrightText: not include the right to compile photos from Unsplash to replicate a similar or competing service.
|
||||
@@ -12,18 +12,64 @@
|
||||
|
||||
#include <FirstTimeUseScreen.h>
|
||||
|
||||
#include <Source/ui_FirstTimeUseScreen.h>
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QIcon>
|
||||
#include <QSpacerItem>
|
||||
|
||||
namespace O3DE::ProjectManager
|
||||
{
|
||||
inline constexpr static int s_contentMargins = 80;
|
||||
inline constexpr static int s_buttonSpacing = 30;
|
||||
inline constexpr static int s_iconSize = 24;
|
||||
inline constexpr static int s_spacerSize = 20;
|
||||
inline constexpr static int s_boxButtonWidth = 210;
|
||||
inline constexpr static int s_boxButtonHeight = 280;
|
||||
|
||||
FirstTimeUseScreen::FirstTimeUseScreen(QWidget* parent)
|
||||
: ScreenWidget(parent)
|
||||
, m_ui(new Ui::FirstTimeUseClass())
|
||||
{
|
||||
m_ui->setupUi(this);
|
||||
QVBoxLayout* vLayout = new QVBoxLayout();
|
||||
setLayout(vLayout);
|
||||
vLayout->setContentsMargins(s_contentMargins, s_contentMargins, s_contentMargins, s_contentMargins);
|
||||
|
||||
connect(m_ui->createProjectButton, &QPushButton::pressed, this, &FirstTimeUseScreen::HandleNewProjectButton);
|
||||
connect(m_ui->openProjectButton, &QPushButton::pressed, this, &FirstTimeUseScreen::HandleOpenProjectButton);
|
||||
QLabel* titleLabel = new QLabel(this);
|
||||
titleLabel->setText(tr("Ready. Set. Create!"));
|
||||
titleLabel->setStyleSheet("font-size: 60px");
|
||||
vLayout->addWidget(titleLabel);
|
||||
|
||||
QLabel* introLabel = new QLabel(this);
|
||||
introLabel->setTextFormat(Qt::AutoText);
|
||||
introLabel->setText(tr("<html><head/><body><p>Welcome to O3DE! Start something new by creating a project. Not sure what to create? </p><p>Explore what\342\200\231s available by downloading our sample project.</p></body></html>"));
|
||||
introLabel->setStyleSheet("font-size: 14px");
|
||||
vLayout->addWidget(introLabel);
|
||||
|
||||
QHBoxLayout* buttonLayout = new QHBoxLayout();
|
||||
buttonLayout->setSpacing(s_buttonSpacing);
|
||||
|
||||
m_createProjectButton = CreateLargeBoxButton(QIcon(":/Resources/Add.svg"), tr("Create Project"), this);
|
||||
m_createProjectButton->setIconSize(QSize(s_iconSize, s_iconSize));
|
||||
buttonLayout->addWidget(m_createProjectButton);
|
||||
|
||||
m_addProjectButton = CreateLargeBoxButton(QIcon(":/Resources/Select_Folder.svg"), tr("Add a Project"), this);
|
||||
m_addProjectButton->setIconSize(QSize(s_iconSize, s_iconSize));
|
||||
buttonLayout->addWidget(m_addProjectButton);
|
||||
|
||||
QSpacerItem* buttonSpacer = new QSpacerItem(s_spacerSize, s_spacerSize, QSizePolicy::Expanding, QSizePolicy::Minimum);
|
||||
buttonLayout->addItem(buttonSpacer);
|
||||
|
||||
vLayout->addItem(buttonLayout);
|
||||
|
||||
QSpacerItem* verticalSpacer = new QSpacerItem(s_spacerSize, s_spacerSize, QSizePolicy::Minimum, QSizePolicy::Expanding);
|
||||
vLayout->addItem(verticalSpacer);
|
||||
|
||||
// Using border-image allows for scaling options background-image does not support
|
||||
setStyleSheet("O3DE--ProjectManager--ScreenWidget { border-image: url(:/Resources/Backgrounds/FirstTimeBackgroundImage.jpg) repeat repeat; }");
|
||||
|
||||
connect(m_createProjectButton, &QPushButton::pressed, this, &FirstTimeUseScreen::HandleNewProjectButton);
|
||||
connect(m_addProjectButton, &QPushButton::pressed, this, &FirstTimeUseScreen::HandleAddProjectButton);
|
||||
}
|
||||
|
||||
ProjectManagerScreen FirstTimeUseScreen::GetScreenEnum()
|
||||
@@ -36,9 +82,21 @@ namespace O3DE::ProjectManager
|
||||
emit ResetScreenRequest(ProjectManagerScreen::NewProjectSettingsCore);
|
||||
emit ChangeScreenRequest(ProjectManagerScreen::NewProjectSettingsCore);
|
||||
}
|
||||
void FirstTimeUseScreen::HandleOpenProjectButton()
|
||||
void FirstTimeUseScreen::HandleAddProjectButton()
|
||||
{
|
||||
emit ChangeScreenRequest(ProjectManagerScreen::ProjectsHome);
|
||||
}
|
||||
|
||||
QPushButton* FirstTimeUseScreen::CreateLargeBoxButton(const QIcon& icon, const QString& text, QWidget* parent)
|
||||
{
|
||||
QPushButton* largeBoxButton = new QPushButton(icon, text, parent);
|
||||
|
||||
largeBoxButton->setFixedSize(s_boxButtonWidth, s_boxButtonHeight);
|
||||
largeBoxButton->setFlat(true);
|
||||
largeBoxButton->setFocusPolicy(Qt::FocusPolicy::NoFocus);
|
||||
largeBoxButton->setStyleSheet("QPushButton { font-size: 14px; background-color: rgba(0, 0, 0, 191); }");
|
||||
|
||||
return largeBoxButton;
|
||||
}
|
||||
|
||||
} // namespace O3DE::ProjectManager
|
||||
|
||||
@@ -15,10 +15,8 @@
|
||||
#include <ScreenWidget.h>
|
||||
#endif
|
||||
|
||||
namespace Ui
|
||||
{
|
||||
class FirstTimeUseClass;
|
||||
}
|
||||
QT_FORWARD_DECLARE_CLASS(QIcon)
|
||||
QT_FORWARD_DECLARE_CLASS(QPushButton)
|
||||
|
||||
namespace O3DE::ProjectManager
|
||||
{
|
||||
@@ -32,10 +30,13 @@ namespace O3DE::ProjectManager
|
||||
|
||||
protected slots:
|
||||
void HandleNewProjectButton();
|
||||
void HandleOpenProjectButton();
|
||||
void HandleAddProjectButton();
|
||||
|
||||
private:
|
||||
QScopedPointer<Ui::FirstTimeUseClass> m_ui;
|
||||
QPushButton* CreateLargeBoxButton(const QIcon& icon, const QString& text, QWidget* parent = nullptr);
|
||||
|
||||
QPushButton* m_createProjectButton;
|
||||
QPushButton* m_addProjectButton;
|
||||
};
|
||||
|
||||
} // namespace O3DE::ProjectManager
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>FirstTimeUseClass</class>
|
||||
<widget class="QWidget" name="FirstTimeUseClass">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>881</width>
|
||||
<height>555</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_5">
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>30</pointsize>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>READY. SET. CREATE!</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string><html><head/><body><p>Welcome to O3DE! Start something new by creating a project. Not sure what to create? </p><p>Explore what’s available by downloading our sample project.</p></body></html></string>
|
||||
</property>
|
||||
<property name="textFormat">
|
||||
<enum>Qt::AutoText</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_7">
|
||||
<item>
|
||||
<widget class="QPushButton" name="createProjectButton">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Create Project</string>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset resource="../project_manager.qrc">
|
||||
<normaloff>:/Resources/Add.svg</normaloff>:/Resources/Add.svg</iconset>
|
||||
</property>
|
||||
<property name="iconSize">
|
||||
<size>
|
||||
<width>16</width>
|
||||
<height>16</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="openProjectButton">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Open a Project</string>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset resource="../project_manager.qrc">
|
||||
<normaloff>:/Resources/Select_Folder.svg</normaloff>:/Resources/Select_Folder.svg</iconset>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources>
|
||||
<include location="../project_manager.qrc"/>
|
||||
</resources>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -27,6 +27,12 @@ namespace O3DE::ProjectManager
|
||||
, m_ui(new Ui::ProjectManagerWindowClass())
|
||||
{
|
||||
m_ui->setupUi(this);
|
||||
QLayout* layout = m_ui->centralWidget->layout();
|
||||
layout->setMargin(0);
|
||||
layout->setSpacing(0);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
|
||||
setFixedSize(this->geometry().width(), this->geometry().height());
|
||||
|
||||
m_pythonBindings = AZStd::make_unique<PythonBindings>(engineRootPath);
|
||||
|
||||
|
||||
@@ -6,10 +6,16 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>800</width>
|
||||
<height>600</height>
|
||||
<width>1200</width>
|
||||
<height>800</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>O3DE Project Manager</string>
|
||||
</property>
|
||||
@@ -21,7 +27,7 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>800</width>
|
||||
<width>1200</width>
|
||||
<height>36</height>
|
||||
</rect>
|
||||
</property>
|
||||
|
||||
@@ -15,18 +15,20 @@
|
||||
#include <ScreenDefs.h>
|
||||
|
||||
#include <QWidget>
|
||||
#include <QStyleOption>
|
||||
#include <QPainter>
|
||||
#endif
|
||||
|
||||
namespace O3DE::ProjectManager
|
||||
{
|
||||
class ScreenWidget
|
||||
: public QWidget
|
||||
: public QFrame
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ScreenWidget(QWidget* parent = nullptr)
|
||||
: QWidget(parent)
|
||||
: QFrame(parent)
|
||||
{
|
||||
}
|
||||
~ScreenWidget() = default;
|
||||
|
||||
@@ -22,6 +22,9 @@ namespace O3DE::ProjectManager
|
||||
: QWidget(parent)
|
||||
{
|
||||
QVBoxLayout* vLayout = new QVBoxLayout();
|
||||
vLayout->setMargin(0);
|
||||
vLayout->setSpacing(0);
|
||||
vLayout->setContentsMargins(0, 0, 0, 0);
|
||||
setLayout(vLayout);
|
||||
|
||||
m_screenStack = new QStackedWidget();
|
||||
|
||||
@@ -9,5 +9,6 @@
|
||||
<file>Resources/iOS.svg</file>
|
||||
<file>Resources/Linux.svg</file>
|
||||
<file>Resources/macOS.svg</file>
|
||||
<file>Resources/Backgrounds/FirstTimeBackgroundImage.jpg</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
||||
@@ -22,7 +22,6 @@ set(FILES
|
||||
Source/EngineInfo.cpp
|
||||
Source/FirstTimeUseScreen.h
|
||||
Source/FirstTimeUseScreen.cpp
|
||||
Source/FirstTimeUseScreen.ui
|
||||
Source/ProjectManagerWindow.h
|
||||
Source/ProjectManagerWindow.cpp
|
||||
Source/ProjectTemplateInfo.h
|
||||
|
||||
@@ -260,7 +260,7 @@ namespace AZ
|
||||
{
|
||||
AZ_TraceContext("Importer", "Animation");
|
||||
|
||||
aiNode* currentNode = context.m_sourceNode.GetAssImpNode();
|
||||
const aiNode* currentNode = context.m_sourceNode.GetAssImpNode();
|
||||
const aiScene* scene = context.m_sourceScene.GetAssImpScene();
|
||||
|
||||
// Add check for animation layers at the scene level.
|
||||
@@ -387,11 +387,10 @@ namespace AZ
|
||||
}
|
||||
|
||||
Events::ProcessingResultCombiner combinedAnimationResult;
|
||||
for (AZ::u32 meshIndex = 0; meshIndex < currentNode->mNumMeshes; ++meshIndex)
|
||||
if (context.m_sourceNode.ContainsMesh())
|
||||
{
|
||||
aiMesh* mesh = scene->mMeshes[currentNode->mMeshes[meshIndex]];
|
||||
|
||||
if (NodeToChannelToMorphAnim::iterator channelsForMeshName = meshMorphAnimations.find(mesh->mName.C_Str());
|
||||
const aiMesh* firstMesh = scene->mMeshes[currentNode->mMeshes[0]];
|
||||
if (NodeToChannelToMorphAnim::iterator channelsForMeshName = meshMorphAnimations.find(firstMesh->mName.C_Str());
|
||||
channelsForMeshName != meshMorphAnimations.end())
|
||||
{
|
||||
const auto [nodeIterName, channels] = *channelsForMeshName;
|
||||
@@ -399,7 +398,7 @@ namespace AZ
|
||||
{
|
||||
const auto& [animation, morphAnimation] = animAndMorphAnim;
|
||||
combinedAnimationResult += ImportBlendShapeAnimation(
|
||||
context, animation, morphAnimation, mesh);
|
||||
context, animation, morphAnimation, firstMesh);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -413,32 +412,39 @@ namespace AZ
|
||||
if (boneAnimations.empty() && !meshMorphAnimations.empty())
|
||||
{
|
||||
const aiAnimation* animation = scene->mAnimations[0];
|
||||
|
||||
// Morph animations need a regular animation on the node, as well.
|
||||
// If there is no bone animation on the current node, then generate one here.
|
||||
AZStd::shared_ptr<SceneData::GraphData::AnimationData> createdAnimationData =
|
||||
AZStd::make_shared<SceneData::GraphData::AnimationData>();
|
||||
|
||||
const size_t numKeyframes = animation->mDuration + 1; // +1 because we start at 0 and the last keyframe is at mDuration instead of mDuration-1
|
||||
createdAnimationData->ReserveKeyFrames(numKeyframes);
|
||||
|
||||
const double timeStepBetweenFrames = 1.0 / animation->mTicksPerSecond;
|
||||
createdAnimationData->SetTimeStepBetweenFrames(timeStepBetweenFrames);
|
||||
|
||||
// Set every frame of the animation to the start location of the node.
|
||||
aiMatrix4x4 combinedTransform = GetConcatenatedLocalTransform(currentNode);
|
||||
DataTypes::MatrixType localTransform = AssImpSDKWrapper::AssImpTypeConverter::ToTransform(combinedTransform);
|
||||
context.m_sourceSceneSystem.SwapTransformForUpAxis(localTransform);
|
||||
context.m_sourceSceneSystem.ConvertUnit(localTransform);
|
||||
for (AZ::u32 time = 0; time <= animation->mDuration; ++time)
|
||||
for (AZ::u32 channelIndex = 0; channelIndex < animation->mNumMorphMeshChannels; ++channelIndex)
|
||||
{
|
||||
createdAnimationData->AddKeyFrame(localTransform);
|
||||
const aiMeshMorphAnim* nodeAnim = animation->mMorphMeshChannels[channelIndex];
|
||||
// Morph animations need a regular animation on the node, as well.
|
||||
// If there is no bone animation on the current node, then generate one here.
|
||||
AZStd::shared_ptr<SceneData::GraphData::AnimationData> createdAnimationData =
|
||||
AZStd::make_shared<SceneData::GraphData::AnimationData>();
|
||||
|
||||
const size_t numKeyframes = GetNumKeyFrames(
|
||||
nodeAnim->mNumKeys,
|
||||
animation->mDuration,
|
||||
animation->mTicksPerSecond);
|
||||
createdAnimationData->ReserveKeyFrames(numKeyframes);
|
||||
|
||||
const double timeStepBetweenFrames = 1.0 / animation->mTicksPerSecond;
|
||||
createdAnimationData->SetTimeStepBetweenFrames(timeStepBetweenFrames);
|
||||
|
||||
// Set every frame of the animation to the start location of the node.
|
||||
aiMatrix4x4 combinedTransform = GetConcatenatedLocalTransform(currentNode);
|
||||
DataTypes::MatrixType localTransform = AssImpSDKWrapper::AssImpTypeConverter::ToTransform(combinedTransform);
|
||||
context.m_sourceSceneSystem.SwapTransformForUpAxis(localTransform);
|
||||
context.m_sourceSceneSystem.ConvertUnit(localTransform);
|
||||
for (AZ::u32 time = 0; time <= numKeyframes; ++time)
|
||||
{
|
||||
createdAnimationData->AddKeyFrame(localTransform);
|
||||
}
|
||||
|
||||
const AZStd::string stubBoneAnimForMorphName(AZStd::string::format("%s%s", nodeName.c_str(), nodeAnim->mName.C_Str()));
|
||||
Containers::SceneGraph::NodeIndex addNode = context.m_scene.GetGraph().AddChild(
|
||||
context.m_currentGraphPosition, stubBoneAnimForMorphName.c_str(), AZStd::move(createdAnimationData));
|
||||
context.m_scene.GetGraph().MakeEndPoint(addNode);
|
||||
}
|
||||
|
||||
Containers::SceneGraph::NodeIndex addNode = context.m_scene.GetGraph().AddChild(
|
||||
context.m_currentGraphPosition, nodeName.c_str(), AZStd::move(createdAnimationData));
|
||||
context.m_scene.GetGraph().MakeEndPoint(addNode);
|
||||
|
||||
|
||||
return combinedAnimationResult.GetResult();
|
||||
}
|
||||
decltype(boneAnimations) parentFillerAnimations;
|
||||
@@ -446,8 +452,8 @@ namespace AZ
|
||||
// Go through all the animations and make sure we create animations for bones who's parents don't have an animation
|
||||
for (auto&& anim : boneAnimations)
|
||||
{
|
||||
aiNode* node = scene->mRootNode->FindNode(anim.first.c_str());
|
||||
aiNode* parent = node->mParent;
|
||||
const aiNode* node = scene->mRootNode->FindNode(anim.first.c_str());
|
||||
const aiNode* parent = node->mParent;
|
||||
|
||||
while (parent && parent != scene->mRootNode)
|
||||
{
|
||||
@@ -598,7 +604,8 @@ namespace AZ
|
||||
// Keyframes generated for every single frame of the animation.
|
||||
typedef AZStd::map<int, AZStd::vector<KeyData>> ValueToKeyDataMap;
|
||||
ValueToKeyDataMap valueToKeyDataMap;
|
||||
|
||||
// Key time can be less than zero, normalize to have zero be the lowest time.
|
||||
double keyOffset = 0;
|
||||
for (int keyIdx = 0; keyIdx < meshMorphAnim->mNumKeys; keyIdx++)
|
||||
{
|
||||
aiMeshMorphKey& key = meshMorphAnim->mKeys[keyIdx];
|
||||
@@ -609,6 +616,10 @@ namespace AZ
|
||||
valueToKeyDataMap[currentValue].insert(
|
||||
AZStd::upper_bound(valueToKeyDataMap[currentValue].begin(), valueToKeyDataMap[currentValue].end(),thisKey),
|
||||
thisKey);
|
||||
if (key.mTime < keyOffset)
|
||||
{
|
||||
keyOffset = key.mTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -631,7 +642,7 @@ namespace AZ
|
||||
const double time = GetTimeForFrame(frame, animation->mTicksPerSecond);
|
||||
|
||||
float weight = 0;
|
||||
if (!SampleKeyFrame(weight, keys, keys.size(), time, keyIdx))
|
||||
if (!SampleKeyFrame(weight, keys, keys.size(), time + keyOffset, keyIdx))
|
||||
{
|
||||
return Events::ProcessingResult::Failure;
|
||||
}
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
|
||||
#include <assimp/scene.h>
|
||||
#include <assimp/mesh.h>
|
||||
|
||||
namespace AZ
|
||||
{
|
||||
namespace SceneAPI
|
||||
@@ -44,7 +43,7 @@ namespace AZ
|
||||
SerializeContext* serializeContext = azrtti_cast<SerializeContext*>(context);
|
||||
if (serializeContext)
|
||||
{
|
||||
serializeContext->Class<AssImpBitangentStreamImporter, SceneCore::LoadingComponent>()->Version(2); // LYN-2576
|
||||
serializeContext->Class<AssImpBitangentStreamImporter, SceneCore::LoadingComponent>()->Version(3); // LYN-3250
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,62 +54,79 @@ namespace AZ
|
||||
{
|
||||
return Events::ProcessingResult::Ignored;
|
||||
}
|
||||
aiNode* currentNode = context.m_sourceNode.GetAssImpNode();
|
||||
const aiNode* currentNode = context.m_sourceNode.GetAssImpNode();
|
||||
const aiScene* scene = context.m_sourceScene.GetAssImpScene();
|
||||
|
||||
GetMeshDataFromParentResult meshDataResult(GetMeshDataFromParent(context));
|
||||
if (!meshDataResult.IsSuccess())
|
||||
const auto meshHasTangentsAndBitangents = [&scene](const unsigned int meshIndex)
|
||||
{
|
||||
return meshDataResult.GetError();
|
||||
}
|
||||
const SceneData::GraphData::MeshData* const parentMeshData(meshDataResult.GetValue());
|
||||
return scene->mMeshes[meshIndex]->HasTangentsAndBitangents();
|
||||
};
|
||||
|
||||
size_t vertexCount = parentMeshData->GetVertexCount();
|
||||
|
||||
int sdkMeshIndex = parentMeshData->GetSdkMeshIndex();
|
||||
if (sdkMeshIndex < 0 || sdkMeshIndex >= currentNode->mNumMeshes)
|
||||
{
|
||||
AZ_Error(Utilities::ErrorWindow, false,
|
||||
"Tried to construct bitangent stream attribute for invalid or non-mesh parent data, mesh index is invalid");
|
||||
return Events::ProcessingResult::Failure;
|
||||
}
|
||||
|
||||
aiMesh* mesh = scene->mMeshes[currentNode->mMeshes[sdkMeshIndex]];
|
||||
|
||||
if (!mesh->HasTangentsAndBitangents())
|
||||
// If there are no bitangents on any meshes, there's nothing to import in this function.
|
||||
const bool anyMeshHasTangentsAndBitangents = AZStd::any_of(currentNode->mMeshes, currentNode->mMeshes + currentNode->mNumMeshes, meshHasTangentsAndBitangents);
|
||||
if (!anyMeshHasTangentsAndBitangents)
|
||||
{
|
||||
return Events::ProcessingResult::Ignored;
|
||||
}
|
||||
|
||||
// AssImp nodes with multiple meshes on them occur when AssImp split a mesh on material.
|
||||
// This logic recombines those meshes to minimize the changes needed to replace FBX SDK with AssImp, FBX SDK did not separate meshes,
|
||||
// and the engine has code to do this later.
|
||||
const bool allMeshesHaveTangentsAndBitangents = AZStd::all_of(currentNode->mMeshes, currentNode->mMeshes + currentNode->mNumMeshes, meshHasTangentsAndBitangents);
|
||||
if (!allMeshesHaveTangentsAndBitangents)
|
||||
{
|
||||
const char* mixedBitangentsError =
|
||||
"Node with name %s has meshes with and without bitangents. "
|
||||
"Placeholder incorrect bitangents will be generated to allow the data to process, "
|
||||
"but the source art needs to be fixed to correct this. Either apply bitangents to all meshes on this node, "
|
||||
"or remove all bitangents from all meshes on this node.";
|
||||
AZ_Error(
|
||||
Utilities::ErrorWindow, false, mixedBitangentsError, currentNode->mName.C_Str());
|
||||
}
|
||||
|
||||
const uint64_t vertexCount = GetVertexCountForAllMeshesOnNode(*currentNode, *scene);
|
||||
|
||||
AZStd::shared_ptr<SceneData::GraphData::MeshVertexBitangentData> bitangentStream =
|
||||
AZStd::make_shared<AZ::SceneData::GraphData::MeshVertexBitangentData>();
|
||||
|
||||
// AssImp only has one bitangentStream per mesh.
|
||||
bitangentStream->SetBitangentSetIndex(0);
|
||||
|
||||
bitangentStream->SetTangentSpace(AZ::SceneAPI::DataTypes::TangentSpace::FromFbx);
|
||||
bitangentStream->ReserveContainerSpace(vertexCount);
|
||||
|
||||
for (int v = 0; v < mesh->mNumVertices; ++v)
|
||||
for (int sdkMeshIndex = 0; sdkMeshIndex < currentNode->mNumMeshes; ++sdkMeshIndex)
|
||||
{
|
||||
const Vector3 bitangent(
|
||||
AssImpSDKWrapper::AssImpTypeConverter::ToVector3(mesh->mBitangents[v]));
|
||||
bitangentStream->AppendBitangent(bitangent);
|
||||
const aiMesh* mesh = scene->mMeshes[currentNode->mMeshes[sdkMeshIndex]];
|
||||
|
||||
for (int v = 0; v < mesh->mNumVertices; ++v)
|
||||
{
|
||||
if (!mesh->HasTangentsAndBitangents())
|
||||
{
|
||||
// This node has mixed meshes with and without bitangents.
|
||||
// An error was already thrown above. Output stub bitangents so
|
||||
// the mesh can still be output in some form, even if the data isn't correct.
|
||||
// The bitangent count needs to match the vertex count on the associated mesh node.
|
||||
bitangentStream->AppendBitangent(Vector3::CreateAxisY());
|
||||
}
|
||||
else
|
||||
{
|
||||
const Vector3 bitangent(
|
||||
AssImpSDKWrapper::AssImpTypeConverter::ToVector3(mesh->mBitangents[v]));
|
||||
bitangentStream->AppendBitangent(bitangent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AZStd::string nodeName(AZStd::string::format("%s",m_defaultNodeName));
|
||||
Containers::SceneGraph::NodeIndex newIndex =
|
||||
context.m_scene.GetGraph().AddChild(context.m_currentGraphPosition, nodeName.c_str());
|
||||
context.m_scene.GetGraph().AddChild(context.m_currentGraphPosition, m_defaultNodeName);
|
||||
|
||||
Events::ProcessingResult bitangentResults;
|
||||
AssImpSceneAttributeDataPopulatedContext dataPopulated(context, bitangentStream, newIndex, nodeName.c_str());
|
||||
AssImpSceneAttributeDataPopulatedContext dataPopulated(context, bitangentStream, newIndex, m_defaultNodeName);
|
||||
bitangentResults = Events::Process(dataPopulated);
|
||||
|
||||
if (bitangentResults != Events::ProcessingResult::Failure)
|
||||
{
|
||||
bitangentResults = AddAttributeDataNodeWithContexts(dataPopulated);
|
||||
}
|
||||
|
||||
return bitangentResults;
|
||||
}
|
||||
|
||||
|
||||
@@ -74,37 +74,51 @@ namespace AZ
|
||||
{
|
||||
return meshDataResult.GetError();
|
||||
}
|
||||
const SceneData::GraphData::MeshData* const parentMeshData(meshDataResult.GetValue());
|
||||
int parentMeshIndex = parentMeshData->GetSdkMeshIndex();
|
||||
|
||||
Events::ProcessingResultCombiner combinedBlendShapeResult;
|
||||
|
||||
// 1. Loop through meshes & anims
|
||||
// Create storage: Anim to meshes
|
||||
// 2. Loop through anims & meshes
|
||||
// Create an anim mesh for each anim, with meshes re-combined.
|
||||
// AssImp separates meshes that have multiple materials.
|
||||
// This code re-combines them to match previous FBX SDK behavior,
|
||||
// so they can be separated by engine code instead.
|
||||
AZStd::map<AZStd::string_view, AZStd::vector<AZStd::pair<int, int>>> animToMeshToAnimMeshIndices;
|
||||
for (int nodeMeshIdx = 0; nodeMeshIdx < numMesh; nodeMeshIdx++)
|
||||
{
|
||||
int sceneMeshIdx = context.m_sourceNode.GetAssImpNode()->mMeshes[nodeMeshIdx];
|
||||
const aiMesh* aiMesh = context.m_sourceScene.GetAssImpScene()->mMeshes[sceneMeshIdx];
|
||||
|
||||
// Each mesh gets its own node in the scene graph, so only generate
|
||||
// morph targets for the current mesh.
|
||||
if (parentMeshIndex != nodeMeshIdx || !aiMesh->mNumAnimMeshes)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int animIdx = 0; animIdx < aiMesh->mNumAnimMeshes; animIdx++)
|
||||
{
|
||||
AZStd::shared_ptr<SceneData::GraphData::BlendShapeData> blendShapeData =
|
||||
AZStd::make_shared<SceneData::GraphData::BlendShapeData>();
|
||||
|
||||
aiAnimMesh* aiAnimMesh = aiMesh->mAnimMeshes[animIdx];
|
||||
AZStd::string nodeName(aiAnimMesh->mName.C_Str());
|
||||
size_t dotIndex = nodeName.rfind('.');
|
||||
if (dotIndex != AZStd::string::npos)
|
||||
{
|
||||
nodeName.erase(0, dotIndex + 1);
|
||||
}
|
||||
RenamedNodesMap::SanitizeNodeName(nodeName, context.m_scene.GetGraph(), context.m_currentGraphPosition, "BlendShape");
|
||||
AZ_TraceContext("Blend shape name", nodeName);
|
||||
animToMeshToAnimMeshIndices[aiAnimMesh->mName.C_Str()].emplace_back(nodeMeshIdx, animIdx);
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& animToMeshIndex : animToMeshToAnimMeshIndices)
|
||||
{
|
||||
AZStd::shared_ptr<SceneData::GraphData::BlendShapeData> blendShapeData =
|
||||
AZStd::make_shared<SceneData::GraphData::BlendShapeData>();
|
||||
|
||||
// Some DCC tools, like Maya, include a full path separated by '.' in the node names.
|
||||
// For example, "cone_skin_blendShapeNode.cone_squash"
|
||||
// Downstream processing doesn't want anything but the last part of that node name,
|
||||
// so find the last '.' and remove anything before it.
|
||||
AZStd::string nodeName(animToMeshIndex.first);
|
||||
size_t dotIndex = nodeName.rfind('.');
|
||||
if (dotIndex != AZStd::string::npos)
|
||||
{
|
||||
nodeName.erase(0, dotIndex + 1);
|
||||
}
|
||||
int vertexOffset = 0;
|
||||
RenamedNodesMap::SanitizeNodeName(nodeName, context.m_scene.GetGraph(), context.m_currentGraphPosition, "BlendShape");
|
||||
AZ_TraceContext("Blend shape name", nodeName);
|
||||
for (const auto& meshIndex : animToMeshIndex.second)
|
||||
{
|
||||
int sceneMeshIdx = context.m_sourceNode.GetAssImpNode()->mMeshes[meshIndex.first];
|
||||
const aiMesh* aiMesh = context.m_sourceScene.GetAssImpScene()->mMeshes[sceneMeshIdx];
|
||||
const aiAnimMesh* aiAnimMesh = aiMesh->mAnimMeshes[meshIndex.second];
|
||||
|
||||
AZStd::bitset<SceneData::GraphData::BlendShapeData::MaxNumUVSets> uvSetUsedFlags;
|
||||
for (AZ::u8 uvSetIndex = 0; uvSetIndex < SceneData::GraphData::BlendShapeData::MaxNumUVSets; ++uvSetIndex)
|
||||
@@ -128,7 +142,7 @@ namespace AZ
|
||||
context.m_sourceSceneSystem.ConvertUnit(vertex);
|
||||
|
||||
blendShapeData->AddPosition(vertex);
|
||||
blendShapeData->SetVertexIndexToControlPointIndexMap(vertIdx, vertIdx);
|
||||
blendShapeData->SetVertexIndexToControlPointIndexMap(vertIdx + vertexOffset, vertIdx + vertexOffset);
|
||||
|
||||
// Add normals
|
||||
if (aiAnimMesh->HasNormals())
|
||||
@@ -191,33 +205,36 @@ namespace AZ
|
||||
}
|
||||
for (int idx = 0; idx < face.mNumIndices; ++idx)
|
||||
{
|
||||
blendFace.vertexIndex[idx] = face.mIndices[idx];
|
||||
blendFace.vertexIndex[idx] = face.mIndices[idx] + vertexOffset;
|
||||
}
|
||||
|
||||
blendShapeData->AddFace(blendFace);
|
||||
}
|
||||
vertexOffset += aiMesh->mNumVertices;
|
||||
|
||||
// Report problem if no vertex or face converted to MeshData
|
||||
if (blendShapeData->GetVertexCount() <= 0 || blendShapeData->GetFaceCount() <= 0)
|
||||
{
|
||||
AZ_Error(Utilities::ErrorWindow, false, "Missing geometry data in blendshape node %s.", nodeName.c_str());
|
||||
return Events::ProcessingResult::Failure;
|
||||
}
|
||||
|
||||
Containers::SceneGraph::NodeIndex newIndex =
|
||||
context.m_scene.GetGraph().AddChild(context.m_currentGraphPosition, nodeName.c_str());
|
||||
|
||||
Events::ProcessingResult blendShapeResult;
|
||||
AssImpSceneAttributeDataPopulatedContext dataPopulated(context, blendShapeData, newIndex, nodeName);
|
||||
blendShapeResult = Events::Process(dataPopulated);
|
||||
|
||||
if (blendShapeResult != Events::ProcessingResult::Failure)
|
||||
{
|
||||
blendShapeResult = AddAttributeDataNodeWithContexts(dataPopulated);
|
||||
}
|
||||
combinedBlendShapeResult += blendShapeResult;
|
||||
}
|
||||
|
||||
|
||||
// Report problem if no vertex or face converted to MeshData
|
||||
if (blendShapeData->GetVertexCount() <= 0 || blendShapeData->GetFaceCount() <= 0)
|
||||
{
|
||||
AZ_Error(Utilities::ErrorWindow, false, "Missing geometry data in blendshape node %s.", nodeName.c_str());
|
||||
return Events::ProcessingResult::Failure;
|
||||
}
|
||||
|
||||
Containers::SceneGraph::NodeIndex newIndex =
|
||||
context.m_scene.GetGraph().AddChild(context.m_currentGraphPosition, nodeName.c_str());
|
||||
|
||||
Events::ProcessingResult blendShapeResult;
|
||||
AssImpSceneAttributeDataPopulatedContext dataPopulated(context, blendShapeData, newIndex, nodeName);
|
||||
blendShapeResult = Events::Process(dataPopulated);
|
||||
|
||||
if (blendShapeResult != Events::ProcessingResult::Failure)
|
||||
{
|
||||
blendShapeResult = AddAttributeDataNodeWithContexts(dataPopulated);
|
||||
}
|
||||
combinedBlendShapeResult += blendShapeResult;
|
||||
}
|
||||
|
||||
return combinedBlendShapeResult.GetResult();
|
||||
|
||||
@@ -46,8 +46,8 @@ namespace AZ
|
||||
}
|
||||
|
||||
void EnumBonesInNode(
|
||||
const aiScene* scene, const aiNode* node, AZStd::unordered_map<AZStd::string, aiNode*>& mainBoneList,
|
||||
AZStd::unordered_map<AZStd::string, aiBone*>& boneLookup)
|
||||
const aiScene* scene, const aiNode* node, AZStd::unordered_map<AZStd::string, const aiNode*>& mainBoneList,
|
||||
AZStd::unordered_map<AZStd::string, const aiBone*>& boneLookup)
|
||||
{
|
||||
/* From AssImp Documentation
|
||||
a) Create a map or a similar container to store which nodes are necessary for the skeleton. Pre-initialise it for all nodes with a "no".
|
||||
@@ -62,14 +62,14 @@ namespace AZ
|
||||
|
||||
for (unsigned meshIndex = 0; meshIndex < node->mNumMeshes; ++meshIndex)
|
||||
{
|
||||
aiMesh* mesh = scene->mMeshes[node->mMeshes[meshIndex]];
|
||||
const aiMesh* mesh = scene->mMeshes[node->mMeshes[meshIndex]];
|
||||
|
||||
for (unsigned boneIndex = 0; boneIndex < mesh->mNumBones; ++boneIndex)
|
||||
{
|
||||
aiBone* bone = mesh->mBones[boneIndex];
|
||||
const aiBone* bone = mesh->mBones[boneIndex];
|
||||
|
||||
aiNode* boneNode = scene->mRootNode->FindNode(bone->mName);
|
||||
aiNode* boneParent = boneNode->mParent;
|
||||
const aiNode* boneNode = scene->mRootNode->FindNode(bone->mName);
|
||||
const aiNode* boneParent = boneNode->mParent;
|
||||
|
||||
mainBoneList[bone->mName.C_Str()] = boneNode;
|
||||
boneLookup[bone->mName.C_Str()] = bone;
|
||||
@@ -85,8 +85,8 @@ namespace AZ
|
||||
}
|
||||
|
||||
void EnumChildren(
|
||||
const aiScene* scene, const aiNode* node, AZStd::unordered_map<AZStd::string, aiNode*>& mainBoneList,
|
||||
AZStd::unordered_map<AZStd::string, aiBone*>& boneLookup)
|
||||
const aiScene* scene, const aiNode* node, AZStd::unordered_map<AZStd::string, const aiNode*>& mainBoneList,
|
||||
AZStd::unordered_map<AZStd::string, const aiBone*>& boneLookup)
|
||||
{
|
||||
EnumBonesInNode(scene, node, mainBoneList, boneLookup);
|
||||
|
||||
@@ -102,7 +102,7 @@ namespace AZ
|
||||
{
|
||||
AZ_TraceContext("Importer", "Bone");
|
||||
|
||||
aiNode* currentNode = context.m_sourceNode.GetAssImpNode();
|
||||
const aiNode* currentNode = context.m_sourceNode.GetAssImpNode();
|
||||
const aiScene* scene = context.m_sourceScene.GetAssImpScene();
|
||||
|
||||
if (IsPivotNode(currentNode->mName))
|
||||
@@ -118,8 +118,8 @@ namespace AZ
|
||||
}
|
||||
else
|
||||
{
|
||||
AZStd::unordered_map<AZStd::string, aiNode*> mainBoneList;
|
||||
AZStd::unordered_map<AZStd::string, aiBone*> boneLookup;
|
||||
AZStd::unordered_map<AZStd::string, const aiNode*> mainBoneList;
|
||||
AZStd::unordered_map<AZStd::string, const aiBone*> boneLookup;
|
||||
EnumChildren(scene, scene->mRootNode, mainBoneList, boneLookup);
|
||||
|
||||
if (mainBoneList.find(currentNode->mName.C_Str()) != mainBoneList.end())
|
||||
@@ -172,7 +172,7 @@ namespace AZ
|
||||
}
|
||||
|
||||
aiMatrix4x4 transform = currentNode->mTransformation;
|
||||
aiNode* parent = currentNode->mParent;
|
||||
const aiNode* parent = currentNode->mParent;
|
||||
|
||||
while (parent)
|
||||
{
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
*/
|
||||
|
||||
#include <AzCore/Serialization/SerializeContext.h>
|
||||
#include <AzCore/std/numeric.h>
|
||||
#include <AzCore/std/smart_ptr/make_shared.h>
|
||||
#include <AzToolsFramework/Debug/TraceContext.h>
|
||||
#include <SceneAPI/FbxSceneBuilder/Importers/AssImpColorStreamImporter.h>
|
||||
@@ -44,7 +45,7 @@ namespace AZ
|
||||
SerializeContext* serializeContext = azrtti_cast<SerializeContext*>(context);
|
||||
if (serializeContext)
|
||||
{
|
||||
serializeContext->Class<AssImpColorStreamImporter, SceneCore::LoadingComponent>()->Version(2); // LYN-2576
|
||||
serializeContext->Class<AssImpColorStreamImporter, SceneCore::LoadingComponent>()->Version(3); // LYN-3250
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,43 +56,64 @@ namespace AZ
|
||||
{
|
||||
return Events::ProcessingResult::Ignored;
|
||||
}
|
||||
aiNode* currentNode = context.m_sourceNode.GetAssImpNode();
|
||||
const aiNode* currentNode = context.m_sourceNode.GetAssImpNode();
|
||||
const aiScene* scene = context.m_sourceScene.GetAssImpScene();
|
||||
|
||||
GetMeshDataFromParentResult meshDataResult(GetMeshDataFromParent(context));
|
||||
if (!meshDataResult.IsSuccess())
|
||||
{
|
||||
return meshDataResult.GetError();
|
||||
}
|
||||
const SceneData::GraphData::MeshData* const parentMeshData(meshDataResult.GetValue());
|
||||
// This node has at least one mesh, verify that the color channel counts are the same for all meshes.
|
||||
const int expectedColorChannels = scene->mMeshes[currentNode->mMeshes[0]]->GetNumColorChannels();
|
||||
const bool allMeshesHaveSameNumberOfColorChannels =
|
||||
AZStd::all_of(currentNode->mMeshes + 1, currentNode->mMeshes + currentNode->mNumMeshes, [scene, expectedColorChannels](const unsigned int meshIndex)
|
||||
{
|
||||
return scene->mMeshes[meshIndex]->GetNumColorChannels() == expectedColorChannels;
|
||||
});
|
||||
|
||||
size_t vertexCount = parentMeshData->GetVertexCount();
|
||||
AZ_Error(
|
||||
Utilities::ErrorWindow,
|
||||
allMeshesHaveSameNumberOfColorChannels,
|
||||
"Color channel counts for node %s has meshes with different color channel counts. "
|
||||
"The color channel count for the first mesh will be used, and placeholder incorrect color values "
|
||||
"will be generated to allow the data to process, but the source art needs to be fixed to correct this. "
|
||||
"All meshes on this node should have the same number of color channels.",
|
||||
currentNode->mName.C_Str());
|
||||
|
||||
int sdkMeshIndex = parentMeshData->GetSdkMeshIndex();
|
||||
if (sdkMeshIndex < 0)
|
||||
if (expectedColorChannels == 0)
|
||||
{
|
||||
AZ_Error(Utilities::ErrorWindow, false,
|
||||
"Tried to construct color stream attribute for invalid or non-mesh parent data, mesh index is missing");
|
||||
return Events::ProcessingResult::Failure;
|
||||
return Events::ProcessingResult::Ignored;
|
||||
}
|
||||
|
||||
aiMesh* mesh = scene->mMeshes[currentNode->mMeshes[sdkMeshIndex]];
|
||||
const uint64_t vertexCount = GetVertexCountForAllMeshesOnNode(*currentNode, *scene);
|
||||
|
||||
Events::ProcessingResultCombiner combinedVertexColorResults;
|
||||
for (int colorSetIndex = 0; colorSetIndex < mesh->GetNumColorChannels(); ++colorSetIndex)
|
||||
for (int colorSetIndex = 0; colorSetIndex < expectedColorChannels; ++colorSetIndex)
|
||||
{
|
||||
|
||||
AZStd::shared_ptr<SceneData::GraphData::MeshVertexColorData> vertexColors =
|
||||
AZStd::make_shared<AZ::SceneData::GraphData::MeshVertexColorData>();
|
||||
vertexColors->ReserveContainerSpace(vertexCount);
|
||||
|
||||
for (int v = 0; v < mesh->mNumVertices; ++v)
|
||||
for (int sdkMeshIndex = 0; sdkMeshIndex < currentNode->mNumMeshes; ++sdkMeshIndex)
|
||||
{
|
||||
AZ::SceneAPI::DataTypes::Color vertexColor(
|
||||
AssImpSDKWrapper::AssImpTypeConverter::ToColor(mesh->mColors[colorSetIndex][v]));
|
||||
vertexColors->AppendColor(vertexColor);
|
||||
const aiMesh* mesh = scene->mMeshes[currentNode->mMeshes[sdkMeshIndex]];
|
||||
for (int v = 0; v < mesh->mNumVertices; ++v)
|
||||
{
|
||||
if (colorSetIndex < mesh->GetNumColorChannels())
|
||||
{
|
||||
AZ::SceneAPI::DataTypes::Color vertexColor(
|
||||
AssImpSDKWrapper::AssImpTypeConverter::ToColor(mesh->mColors[colorSetIndex][v]));
|
||||
vertexColors->AppendColor(vertexColor);
|
||||
}
|
||||
else
|
||||
{
|
||||
// An error was already emitted if this mesh has less color channels
|
||||
// than other meshes on the parent node. Append an arbitrary color value, fully opaque black,
|
||||
// so the mesh can still be processed.
|
||||
// It's better to let the engine load a partially valid mesh than to completely fail.
|
||||
vertexColors->AppendColor(AZ::SceneAPI::DataTypes::Color(0.0f,0.0f,0.0f,1.0f));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AZStd::string nodeName(AZStd::string::format("%s%d",m_defaultNodeName,colorSetIndex));
|
||||
AZStd::string nodeName(AZStd::string::format("%s%d", m_defaultNodeName, colorSetIndex));
|
||||
Containers::SceneGraph::NodeIndex newIndex =
|
||||
context.m_scene.GetGraph().AddChild(context.m_currentGraphPosition, nodeName.c_str());
|
||||
|
||||
@@ -106,9 +128,7 @@ namespace AZ
|
||||
|
||||
combinedVertexColorResults += colorMapResults;
|
||||
}
|
||||
|
||||
return combinedVertexColorResults.GetResult();
|
||||
|
||||
}
|
||||
|
||||
} // namespace FbxSceneBuilder
|
||||
|
||||
@@ -69,7 +69,7 @@ namespace AZ
|
||||
|
||||
aiMatrix4x4 GetConcatenatedLocalTransform(const aiNode* currentNode)
|
||||
{
|
||||
aiNode* parent = currentNode->mParent;
|
||||
const aiNode* parent = currentNode->mParent;
|
||||
aiMatrix4x4 combinedTransform = currentNode->mTransformation;
|
||||
|
||||
while (parent)
|
||||
|
||||
@@ -62,7 +62,7 @@ namespace AZ
|
||||
for (int idx = 0; idx < context.m_sourceNode.m_assImpNode->mNumMeshes; ++idx)
|
||||
{
|
||||
int meshIndex = context.m_sourceNode.m_assImpNode->mMeshes[idx];
|
||||
aiMesh* assImpMesh = context.m_sourceScene.GetAssImpScene()->mMeshes[meshIndex];
|
||||
const aiMesh* assImpMesh = context.m_sourceScene.GetAssImpScene()->mMeshes[meshIndex];
|
||||
AZ_Assert(assImpMesh, "Asset Importer Mesh should not be null.");
|
||||
int materialIndex = assImpMesh->mMaterialIndex;
|
||||
AZ_TraceContext("Material Index", materialIndex);
|
||||
|
||||
@@ -45,7 +45,7 @@ namespace AZ
|
||||
{
|
||||
AZ_TraceContext("Importer", "Mesh");
|
||||
|
||||
aiNode* currentNode = context.m_sourceNode.GetAssImpNode();
|
||||
const aiNode* currentNode = context.m_sourceNode.GetAssImpNode();
|
||||
const aiScene* scene = context.m_sourceScene.GetAssImpScene();
|
||||
|
||||
if (!context.m_sourceNode.ContainsMesh() || IsSkinnedMesh(*currentNode, *scene))
|
||||
|
||||
@@ -45,7 +45,7 @@ namespace AZ
|
||||
{
|
||||
AZ_TraceContext("Importer", "Skin");
|
||||
|
||||
aiNode* currentNode = context.m_sourceNode.GetAssImpNode();
|
||||
const aiNode* currentNode = context.m_sourceNode.GetAssImpNode();
|
||||
const aiScene* scene = context.m_sourceScene.GetAssImpScene();
|
||||
|
||||
if (!context.m_sourceNode.ContainsMesh() || !IsSkinnedMesh(*currentNode, *scene))
|
||||
|
||||
@@ -51,7 +51,7 @@ namespace AZ
|
||||
{
|
||||
AZ_TraceContext("Importer", "Skin Weights");
|
||||
|
||||
aiNode* currentNode = context.m_sourceNode.GetAssImpNode();
|
||||
const aiNode* currentNode = context.m_sourceNode.GetAssImpNode();
|
||||
const aiScene* scene = context.m_sourceScene.GetAssImpScene();
|
||||
|
||||
if(currentNode->mNumMeshes <= 0)
|
||||
@@ -59,35 +59,21 @@ namespace AZ
|
||||
return Events::ProcessingResult::Ignored;
|
||||
}
|
||||
|
||||
GetMeshDataFromParentResult meshDataResult(GetMeshDataFromParent(context));
|
||||
if (!meshDataResult.IsSuccess())
|
||||
{
|
||||
return meshDataResult.GetError();
|
||||
}
|
||||
const SceneData::GraphData::MeshData* const parentMeshData(meshDataResult.GetValue());
|
||||
|
||||
int parentMeshIndex = parentMeshData->GetSdkMeshIndex();
|
||||
|
||||
Events::ProcessingResultCombiner combinedSkinWeightsResult;
|
||||
|
||||
// Don't create this until a bone with weights is encountered
|
||||
Containers::SceneGraph::NodeIndex weightsIndexForMesh;
|
||||
AZStd::string skinWeightName;
|
||||
AZStd::shared_ptr<SceneData::GraphData::SkinWeightData> skinWeightData;
|
||||
|
||||
const uint64_t totalVertices = GetVertexCountForAllMeshesOnNode(*currentNode, *scene);
|
||||
|
||||
int vertexCount = 0;
|
||||
for(unsigned nodeMeshIndex = 0; nodeMeshIndex < currentNode->mNumMeshes; ++nodeMeshIndex)
|
||||
{
|
||||
if (nodeMeshIndex != parentMeshIndex)
|
||||
{
|
||||
// Only generate skinning data for the parent mesh.
|
||||
// Each AssImp mesh is assigned to a unique node,
|
||||
// so the skinning data should be generated as a child node
|
||||
// for the associated parent mesh.
|
||||
continue;
|
||||
}
|
||||
int sceneMeshIndex = currentNode->mMeshes[nodeMeshIndex];
|
||||
const aiMesh* mesh = scene->mMeshes[sceneMeshIndex];
|
||||
|
||||
// Don't create this until a bone with weights is encountered
|
||||
Containers::SceneGraph::NodeIndex weightsIndexForMesh;
|
||||
AZStd::string skinWeightName;
|
||||
AZStd::shared_ptr<SceneData::GraphData::SkinWeightData> skinWeightData;
|
||||
|
||||
for(unsigned b = 0; b < mesh->mNumBones; ++b)
|
||||
{
|
||||
const aiBone* bone = mesh->mBones[b];
|
||||
@@ -100,7 +86,6 @@ namespace AZ
|
||||
if (!weightsIndexForMesh.IsValid())
|
||||
{
|
||||
skinWeightName = s_skinWeightName;
|
||||
skinWeightName += AZStd::to_string(nodeMeshIndex);
|
||||
RenamedNodesMap::SanitizeNodeName(skinWeightName, context.m_scene.GetGraph(), context.m_currentGraphPosition);
|
||||
|
||||
weightsIndexForMesh =
|
||||
@@ -116,23 +101,25 @@ namespace AZ
|
||||
}
|
||||
Pending pending;
|
||||
pending.m_bone = bone;
|
||||
pending.m_numVertices = mesh->mNumVertices;
|
||||
pending.m_numVertices = totalVertices;
|
||||
pending.m_skinWeightData = skinWeightData;
|
||||
pending.m_vertOffset = vertexCount;
|
||||
m_pendingSkinWeights.push_back(pending);
|
||||
}
|
||||
|
||||
Events::ProcessingResult skinWeightsResult;
|
||||
AssImpSceneAttributeDataPopulatedContext dataPopulated(context, skinWeightData, weightsIndexForMesh, skinWeightName);
|
||||
skinWeightsResult = Events::Process(dataPopulated);
|
||||
|
||||
if (skinWeightsResult != Events::ProcessingResult::Failure)
|
||||
{
|
||||
skinWeightsResult = AddAttributeDataNodeWithContexts(dataPopulated);
|
||||
}
|
||||
|
||||
combinedSkinWeightsResult += skinWeightsResult;
|
||||
vertexCount += mesh->mNumVertices;
|
||||
}
|
||||
|
||||
Events::ProcessingResult skinWeightsResult;
|
||||
AssImpSceneAttributeDataPopulatedContext dataPopulated(context, skinWeightData, weightsIndexForMesh, skinWeightName);
|
||||
skinWeightsResult = Events::Process(dataPopulated);
|
||||
|
||||
if (skinWeightsResult != Events::ProcessingResult::Failure)
|
||||
{
|
||||
skinWeightsResult = AddAttributeDataNodeWithContexts(dataPopulated);
|
||||
}
|
||||
|
||||
combinedSkinWeightsResult += skinWeightsResult;
|
||||
|
||||
return combinedSkinWeightsResult.GetResult();
|
||||
}
|
||||
|
||||
@@ -153,7 +140,7 @@ namespace AZ
|
||||
link.boneId = boneId;
|
||||
link.weight = it.m_bone->mWeights[weight].mWeight;
|
||||
|
||||
it.m_skinWeightData->AddAndSortLink(it.m_bone->mWeights[weight].mVertexId, link);
|
||||
it.m_skinWeightData->AddAndSortLink(it.m_bone->mWeights[weight].mVertexId + it.m_vertOffset, link);
|
||||
}
|
||||
}
|
||||
const auto result = m_pendingSkinWeights.empty() ? Events::ProcessingResult::Ignored : Events::ProcessingResult::Success;
|
||||
|
||||
@@ -61,6 +61,7 @@ namespace AZ
|
||||
{
|
||||
const aiBone* m_bone = nullptr;
|
||||
unsigned m_numVertices = 0;
|
||||
unsigned m_vertOffset = 0;
|
||||
AZStd::shared_ptr<SceneData::GraphData::SkinWeightData> m_skinWeightData;
|
||||
};
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
*/
|
||||
|
||||
#include <AzCore/Serialization/SerializeContext.h>
|
||||
#include <AzCore/std/numeric.h>
|
||||
#include <AzCore/std/smart_ptr/make_shared.h>
|
||||
#include <AzToolsFramework/Debug/TraceContext.h>
|
||||
#include <SceneAPI/FbxSceneBuilder/Importers/AssImpTangentStreamImporter.h>
|
||||
@@ -44,7 +45,7 @@ namespace AZ
|
||||
SerializeContext* serializeContext = azrtti_cast<SerializeContext*>(context);
|
||||
if (serializeContext)
|
||||
{
|
||||
serializeContext->Class<AssImpTangentStreamImporter, SceneCore::LoadingComponent>()->Version(2); // LYN-2576
|
||||
serializeContext->Class<AssImpTangentStreamImporter, SceneCore::LoadingComponent>()->Version(3); // LYN-3250
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,62 +56,79 @@ namespace AZ
|
||||
{
|
||||
return Events::ProcessingResult::Ignored;
|
||||
}
|
||||
aiNode* currentNode = context.m_sourceNode.GetAssImpNode();
|
||||
const aiNode* currentNode = context.m_sourceNode.GetAssImpNode();
|
||||
const aiScene* scene = context.m_sourceScene.GetAssImpScene();
|
||||
|
||||
GetMeshDataFromParentResult meshDataResult(GetMeshDataFromParent(context));
|
||||
if (!meshDataResult.IsSuccess())
|
||||
|
||||
const auto meshHasTangentsAndBitangents = [&scene](const unsigned int meshIndex)
|
||||
{
|
||||
return meshDataResult.GetError();
|
||||
}
|
||||
const SceneData::GraphData::MeshData* const parentMeshData(meshDataResult.GetValue());
|
||||
return scene->mMeshes[meshIndex]->HasTangentsAndBitangents();
|
||||
};
|
||||
|
||||
size_t vertexCount = parentMeshData->GetVertexCount();
|
||||
|
||||
int sdkMeshIndex = parentMeshData->GetSdkMeshIndex();
|
||||
if (sdkMeshIndex < 0 || sdkMeshIndex >= currentNode->mNumMeshes)
|
||||
{
|
||||
AZ_Error(Utilities::ErrorWindow, false,
|
||||
"Tried to construct tangent stream attribute for invalid or non-mesh parent data, mesh index is invalid");
|
||||
return Events::ProcessingResult::Failure;
|
||||
}
|
||||
|
||||
aiMesh* mesh = scene->mMeshes[currentNode->mMeshes[sdkMeshIndex]];
|
||||
|
||||
if (!mesh->HasTangentsAndBitangents())
|
||||
// If there are no tangents on any meshes, there's nothing to import in this function.
|
||||
const bool anyMeshHasTangentsAndBitangents = AZStd::any_of(currentNode->mMeshes, currentNode->mMeshes + currentNode->mNumMeshes, meshHasTangentsAndBitangents);
|
||||
if (!anyMeshHasTangentsAndBitangents)
|
||||
{
|
||||
return Events::ProcessingResult::Ignored;
|
||||
}
|
||||
|
||||
// AssImp nodes with multiple meshes on them occur when AssImp split a mesh on material.
|
||||
// This logic recombines those meshes to minimize the changes needed to replace FBX SDK with AssImp, FBX SDK did not separate meshes,
|
||||
// and the engine has code to do this later.
|
||||
const bool allMeshesHaveTangentsAndBitangents = AZStd::all_of(currentNode->mMeshes, currentNode->mMeshes + currentNode->mNumMeshes, meshHasTangentsAndBitangents);
|
||||
if (!allMeshesHaveTangentsAndBitangents)
|
||||
{
|
||||
const char* mixedTangentsError =
|
||||
"Node with name %s has meshes with and without tangents. "
|
||||
"Placeholder incorrect tangents will be generated to allow the data to process, "
|
||||
"but the source art needs to be fixed to correct this. Either apply tangents to all meshes on this node, "
|
||||
"or remove all tangents from all meshes on this node.";
|
||||
AZ_Error(
|
||||
Utilities::ErrorWindow, false, mixedTangentsError, currentNode->mName.C_Str());
|
||||
}
|
||||
|
||||
const uint64_t vertexCount = GetVertexCountForAllMeshesOnNode(*currentNode, *scene);
|
||||
|
||||
AZStd::shared_ptr<SceneData::GraphData::MeshVertexTangentData> tangentStream =
|
||||
AZStd::make_shared<AZ::SceneData::GraphData::MeshVertexTangentData>();
|
||||
|
||||
// AssImp only has one tangentStream per mesh.
|
||||
tangentStream->SetTangentSetIndex(0);
|
||||
|
||||
tangentStream->SetTangentSpace(AZ::SceneAPI::DataTypes::TangentSpace::FromFbx);
|
||||
tangentStream->ReserveContainerSpace(vertexCount);
|
||||
|
||||
for (int v = 0; v < mesh->mNumVertices; ++v)
|
||||
for (int sdkMeshIndex = 0; sdkMeshIndex < currentNode->mNumMeshes; ++sdkMeshIndex)
|
||||
{
|
||||
// Vector4's constructor that takes in a vector3 sets w to 1.0f automatically.
|
||||
const Vector4 tangent(AssImpSDKWrapper::AssImpTypeConverter::ToVector3(mesh->mTangents[v]));
|
||||
tangentStream->AppendTangent(tangent);
|
||||
const aiMesh* mesh = scene->mMeshes[currentNode->mMeshes[sdkMeshIndex]];
|
||||
|
||||
for (int v = 0; v < mesh->mNumVertices; ++v)
|
||||
{
|
||||
if (!mesh->HasTangentsAndBitangents())
|
||||
{
|
||||
// This node has mixed meshes with and without tangents.
|
||||
// An error was already thrown above. Output stub tangents so
|
||||
// the mesh can still be output in some form, even if the data isn't correct.
|
||||
// The tangent count needs to match the vertex count on the associated mesh node.
|
||||
tangentStream->AppendTangent(Vector4(0.f, 1.f, 0.f, 1.f));
|
||||
}
|
||||
else
|
||||
{
|
||||
const Vector4 tangent(
|
||||
AssImpSDKWrapper::AssImpTypeConverter::ToVector3(mesh->mTangents[v]));
|
||||
tangentStream->AppendTangent(tangent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AZStd::string nodeName(AZStd::string::format("%s", m_defaultNodeName));
|
||||
Containers::SceneGraph::NodeIndex newIndex =
|
||||
context.m_scene.GetGraph().AddChild(context.m_currentGraphPosition, nodeName.c_str());
|
||||
context.m_scene.GetGraph().AddChild(context.m_currentGraphPosition, m_defaultNodeName);
|
||||
|
||||
Events::ProcessingResult tangentResults;
|
||||
AssImpSceneAttributeDataPopulatedContext dataPopulated(context, tangentStream, newIndex, nodeName.c_str());
|
||||
AssImpSceneAttributeDataPopulatedContext dataPopulated(context, tangentStream, newIndex, m_defaultNodeName);
|
||||
tangentResults = Events::Process(dataPopulated);
|
||||
|
||||
if (tangentResults != Events::ProcessingResult::Failure)
|
||||
{
|
||||
tangentResults = AddAttributeDataNodeWithContexts(dataPopulated);
|
||||
}
|
||||
|
||||
return tangentResults;
|
||||
}
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ namespace AZ
|
||||
Events::ProcessingResult AssImpTransformImporter::ImportTransform(AssImpSceneNodeAppendedContext& context)
|
||||
{
|
||||
AZ_TraceContext("Importer", "transform");
|
||||
aiNode* currentNode = context.m_sourceNode.GetAssImpNode();
|
||||
const aiNode* currentNode = context.m_sourceNode.GetAssImpNode();
|
||||
const aiScene* scene = context.m_sourceScene.GetAssImpScene();
|
||||
|
||||
if (currentNode == scene->mRootNode || IsPivotNode(currentNode->mName))
|
||||
|
||||
@@ -12,17 +12,19 @@
|
||||
|
||||
#include <AzCore/Math/Vector2.h>
|
||||
#include <AzCore/Serialization/SerializeContext.h>
|
||||
#include <AzCore/std/containers/array.h>
|
||||
#include <AzCore/std/numeric.h>
|
||||
#include <AzCore/std/smart_ptr/make_shared.h>
|
||||
#include <AzToolsFramework/Debug/TraceContext.h>
|
||||
#include <SceneAPI/FbxSceneBuilder/ImportContexts/AssImpImportContexts.h>
|
||||
#include <SceneAPI/FbxSceneBuilder/Importers/AssImpUvMapImporter.h>
|
||||
#include <SceneAPI/FbxSceneBuilder/Importers/ImporterUtilities.h>
|
||||
#include <SceneAPI/FbxSceneBuilder/Importers/Utilities/AssImpMeshImporterUtilities.h>
|
||||
#include <SceneAPI/SDKWrapper/AssImpNodeWrapper.h>
|
||||
#include <SceneAPI/SDKWrapper/AssImpSceneWrapper.h>
|
||||
#include <SceneAPI/SceneCore/Utilities/Reporting.h>
|
||||
#include <SceneAPI/SceneData/GraphData/MeshData.h>
|
||||
#include <SceneAPI/SceneData/GraphData/MeshVertexUVData.h>
|
||||
#include <SceneAPI/SceneCore/Utilities/Reporting.h>
|
||||
#include <SceneAPI/SDKWrapper/AssImpNodeWrapper.h>
|
||||
#include <SceneAPI/SDKWrapper/AssImpSceneWrapper.h>
|
||||
|
||||
#include <assimp/scene.h>
|
||||
#include <assimp/mesh.h>
|
||||
@@ -45,7 +47,7 @@ namespace AZ
|
||||
SerializeContext* serializeContext = azrtti_cast<SerializeContext*>(context);
|
||||
if (serializeContext)
|
||||
{
|
||||
serializeContext->Class<AssImpUvMapImporter, SceneCore::LoadingComponent>()->Version(3); // LYN-2506
|
||||
serializeContext->Class<AssImpUvMapImporter, SceneCore::LoadingComponent>()->Version(4); // LYN-3250
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,28 +58,53 @@ namespace AZ
|
||||
{
|
||||
return Events::ProcessingResult::Ignored;
|
||||
}
|
||||
aiNode* currentNode = context.m_sourceNode.GetAssImpNode();
|
||||
const aiNode* currentNode = context.m_sourceNode.GetAssImpNode();
|
||||
const aiScene* scene = context.m_sourceScene.GetAssImpScene();
|
||||
|
||||
GetMeshDataFromParentResult meshDataResult(GetMeshDataFromParent(context));
|
||||
if (!meshDataResult.IsSuccess())
|
||||
// AssImp separates meshes that have multiple materials.
|
||||
// This code re-combines them to match previous FBX SDK behavior,
|
||||
// so they can be separated by engine code instead.
|
||||
bool foundTextureCoordinates = false;
|
||||
AZStd::array<int, AI_MAX_NUMBER_OF_TEXTURECOORDS> meshesPerTextureCoordinateIndex = {};
|
||||
for (int localMeshIndex = 0; localMeshIndex < currentNode->mNumMeshes; ++localMeshIndex)
|
||||
{
|
||||
return meshDataResult.GetError();
|
||||
aiMesh* mesh = scene->mMeshes[currentNode->mMeshes[localMeshIndex]];
|
||||
for (int texCoordIndex = 0; texCoordIndex < meshesPerTextureCoordinateIndex.size(); ++texCoordIndex)
|
||||
{
|
||||
if (!mesh->mTextureCoords[texCoordIndex])
|
||||
{
|
||||
continue;
|
||||
}
|
||||
++meshesPerTextureCoordinateIndex[texCoordIndex];
|
||||
foundTextureCoordinates = true;
|
||||
}
|
||||
}
|
||||
const SceneData::GraphData::MeshData* const parentMeshData(meshDataResult.GetValue());
|
||||
|
||||
size_t vertexCount = parentMeshData->GetVertexCount();
|
||||
if (!foundTextureCoordinates)
|
||||
{
|
||||
return Events::ProcessingResult::Ignored;
|
||||
}
|
||||
|
||||
int sdkMeshIndex = parentMeshData->GetSdkMeshIndex();
|
||||
AZ_Assert(sdkMeshIndex >= 0,
|
||||
"Tried to construct uv stream attribute for invalid or non-mesh parent data, mesh index is missing");
|
||||
const uint64_t vertexCount = GetVertexCountForAllMeshesOnNode(*currentNode, *scene);
|
||||
|
||||
aiMesh* mesh = scene->mMeshes[currentNode->mMeshes[sdkMeshIndex]];
|
||||
for (int texCoordIndex = 0; texCoordIndex < meshesPerTextureCoordinateIndex.size(); ++texCoordIndex)
|
||||
{
|
||||
int meshesWithIndex = meshesPerTextureCoordinateIndex[texCoordIndex];
|
||||
AZ_Error(
|
||||
Utilities::ErrorWindow,
|
||||
meshesWithIndex == 0 || meshesWithIndex == currentNode->mNumMeshes,
|
||||
"Texture coordinate index %d for node %s is not on all meshes on this node. "
|
||||
"Placeholder arbitrary texture values will be generated to allow the data to process, but the source art "
|
||||
"needs to be fixed to correct this. All meshes on this node should have the same number of texture coordinate channels.",
|
||||
texCoordIndex,
|
||||
currentNode->mName.C_Str());
|
||||
}
|
||||
|
||||
Events::ProcessingResultCombiner combinedUvMapResults;
|
||||
for (int texCoordIndex = 0; texCoordIndex < AI_MAX_NUMBER_OF_TEXTURECOORDS; ++texCoordIndex)
|
||||
for (int texCoordIndex = 0; texCoordIndex < meshesPerTextureCoordinateIndex.size(); ++texCoordIndex)
|
||||
{
|
||||
if (!mesh->mTextureCoords[texCoordIndex])
|
||||
// No meshes have this texture coordinate index, skip it.
|
||||
if (meshesPerTextureCoordinateIndex[texCoordIndex] == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -85,24 +112,55 @@ namespace AZ
|
||||
AZStd::shared_ptr<SceneData::GraphData::MeshVertexUVData> uvMap =
|
||||
AZStd::make_shared<AZ::SceneData::GraphData::MeshVertexUVData>();
|
||||
uvMap->ReserveContainerSpace(vertexCount);
|
||||
|
||||
bool customNameFound = false;
|
||||
AZStd::string name(AZStd::string::format("%s%d", m_defaultNodeName, texCoordIndex));
|
||||
if (mesh->mTextureCoordsNames[texCoordIndex].length)
|
||||
for (int sdkMeshIndex = 0; sdkMeshIndex < currentNode->mNumMeshes; ++sdkMeshIndex)
|
||||
{
|
||||
name = mesh->mTextureCoordsNames[texCoordIndex].C_Str();
|
||||
const aiMesh* mesh = scene->mMeshes[currentNode->mMeshes[sdkMeshIndex]];
|
||||
if(mesh->mTextureCoords[texCoordIndex])
|
||||
{
|
||||
if (mesh->mTextureCoordsNames[texCoordIndex].length > 0)
|
||||
{
|
||||
if (!customNameFound)
|
||||
{
|
||||
name = mesh->mTextureCoordsNames[texCoordIndex].C_Str();
|
||||
customNameFound = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
AZ_Warning(Utilities::WarningWindow,
|
||||
strcmp(name.c_str(), mesh->mTextureCoordsNames[texCoordIndex].C_Str()) == 0,
|
||||
"Node %s has conflicting mesh coordinate names at index %d, %s and %s. Using %s.",
|
||||
currentNode->mName.C_Str(),
|
||||
texCoordIndex,
|
||||
name.c_str(),
|
||||
mesh->mTextureCoordsNames[texCoordIndex].C_Str(),
|
||||
name.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int v = 0; v < mesh->mNumVertices; ++v)
|
||||
{
|
||||
if (mesh->mTextureCoords[texCoordIndex])
|
||||
{
|
||||
AZ::Vector2 vertexUV(
|
||||
mesh->mTextureCoords[texCoordIndex][v].x,
|
||||
// The engine's V coordinate is reverse of how it's stored in the FBX file.
|
||||
1.0f - mesh->mTextureCoords[texCoordIndex][v].y);
|
||||
uvMap->AppendUV(vertexUV);
|
||||
}
|
||||
else
|
||||
{
|
||||
// An error was already emitted if the UV channels for all meshes on this node do not match.
|
||||
// Append an arbitrary UV value so that the mesh can still be processed.
|
||||
// It's better to let the engine load a partially valid mesh than to completely fail.
|
||||
uvMap->AppendUV(AZ::Vector2::CreateZero());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uvMap->SetCustomName(name.c_str());
|
||||
|
||||
for (int v = 0; v < mesh->mNumVertices; ++v)
|
||||
{
|
||||
AZ::Vector2 vertexUV(
|
||||
mesh->mTextureCoords[texCoordIndex][v].x,
|
||||
// The engine's V coordinate is reverse of how it's stored in the FBX file.
|
||||
1.0f - mesh->mTextureCoords[texCoordIndex][v].y);
|
||||
uvMap->AppendUV(vertexUV);
|
||||
}
|
||||
|
||||
Containers::SceneGraph::NodeIndex newIndex =
|
||||
context.m_scene.GetGraph().AddChild(context.m_currentGraphPosition, name.c_str());
|
||||
|
||||
@@ -116,6 +174,7 @@ namespace AZ
|
||||
}
|
||||
|
||||
combinedUvMapResults += uvMapResults;
|
||||
|
||||
}
|
||||
|
||||
return combinedUvMapResults.GetResult();
|
||||
|
||||
+24
-12
@@ -13,6 +13,7 @@
|
||||
#include <assimp/mesh.h>
|
||||
#include <assimp/scene.h>
|
||||
#include <AzCore/Casting/numeric_cast.h>
|
||||
#include <AzCore/std/numeric.h>
|
||||
#include <AzCore/std/smart_ptr/make_shared.h>
|
||||
#include <SceneAPI/FbxSceneBuilder/FbxSceneSystem.h>
|
||||
#include <SceneAPI/FbxSceneBuilder/ImportContexts/AssImpImportContexts.h>
|
||||
@@ -24,7 +25,7 @@
|
||||
|
||||
namespace AZ::SceneAPI::FbxSceneBuilder
|
||||
{
|
||||
bool BuildSceneMeshFromAssImpMesh(aiNode* currentNode, const aiScene* scene, const FbxSceneSystem& sceneSystem, AZStd::vector<AZStd::shared_ptr<DataTypes::IGraphObject>>& meshes,
|
||||
bool BuildSceneMeshFromAssImpMesh(const aiNode* currentNode, const aiScene* scene, const FbxSceneSystem& sceneSystem, AZStd::vector<AZStd::shared_ptr<DataTypes::IGraphObject>>& meshes,
|
||||
const AZStd::function<AZStd::shared_ptr<SceneData::GraphData::MeshData>()>& makeMeshFunc)
|
||||
{
|
||||
AZStd::unordered_map<int, int> assImpMatIndexToLYIndex;
|
||||
@@ -34,17 +35,18 @@ namespace AZ::SceneAPI::FbxSceneBuilder
|
||||
{
|
||||
return false;
|
||||
}
|
||||
auto newMesh = makeMeshFunc();
|
||||
|
||||
newMesh->SetUnitSizeInMeters(sceneSystem.GetUnitSizeInMeters());
|
||||
newMesh->SetOriginalUnitSizeInMeters(sceneSystem.GetOriginalUnitSizeInMeters());
|
||||
|
||||
// AssImp separates meshes that have multiple materials.
|
||||
// This code re-combines them to match previous FBX SDK behavior,
|
||||
// so they can be separated by engine code instead.
|
||||
int vertOffset = 0;
|
||||
for (int m = 0; m < currentNode->mNumMeshes; ++m)
|
||||
{
|
||||
auto newMesh = makeMeshFunc();
|
||||
|
||||
newMesh->SetUnitSizeInMeters(sceneSystem.GetUnitSizeInMeters());
|
||||
newMesh->SetOriginalUnitSizeInMeters(sceneSystem.GetOriginalUnitSizeInMeters());
|
||||
|
||||
newMesh->SetSdkMeshIndex(m);
|
||||
|
||||
aiMesh* mesh = scene->mMeshes[currentNode->mMeshes[m]];
|
||||
const aiMesh* mesh = scene->mMeshes[currentNode->mMeshes[m]];
|
||||
|
||||
// Lumberyard materials are created in order based on mesh references in the scene
|
||||
if (assImpMatIndexToLYIndex.find(mesh->mMaterialIndex) == assImpMatIndexToLYIndex.end())
|
||||
@@ -59,7 +61,7 @@ namespace AZ::SceneAPI::FbxSceneBuilder
|
||||
sceneSystem.SwapVec3ForUpAxis(vertex);
|
||||
sceneSystem.ConvertUnit(vertex);
|
||||
newMesh->AddPosition(vertex);
|
||||
newMesh->SetVertexIndexToControlPointIndexMap(vertIdx, vertIdx);
|
||||
newMesh->SetVertexIndexToControlPointIndexMap(vertIdx + vertOffset, vertIdx + vertOffset);
|
||||
|
||||
if (mesh->HasNormals())
|
||||
{
|
||||
@@ -86,14 +88,15 @@ namespace AZ::SceneAPI::FbxSceneBuilder
|
||||
}
|
||||
for (int idx = 0; idx < face.mNumIndices; ++idx)
|
||||
{
|
||||
meshFace.vertexIndex[idx] = face.mIndices[idx];
|
||||
meshFace.vertexIndex[idx] = face.mIndices[idx] + vertOffset;
|
||||
}
|
||||
|
||||
newMesh->AddFace(meshFace, assImpMatIndexToLYIndex[mesh->mMaterialIndex]);
|
||||
}
|
||||
vertOffset += mesh->mNumVertices;
|
||||
|
||||
meshes.push_back(newMesh);
|
||||
}
|
||||
meshes.push_back(newMesh);
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -127,4 +130,13 @@ namespace AZ::SceneAPI::FbxSceneBuilder
|
||||
azrtti_cast<const SceneData::GraphData::MeshData* const>(parentData);
|
||||
return AZ::Success(parentMeshData);
|
||||
}
|
||||
|
||||
uint64_t GetVertexCountForAllMeshesOnNode(const aiNode& node, const aiScene& scene)
|
||||
{
|
||||
return AZStd::accumulate(node.mMeshes, node.mMeshes + node.mNumMeshes, uint64_t{ 0u },
|
||||
[&scene](auto runningTotal, unsigned int meshIndex)
|
||||
{
|
||||
return runningTotal + scene.mMeshes[meshIndex]->mNumVertices;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+6
-1
@@ -44,11 +44,16 @@ namespace AZ
|
||||
|
||||
namespace FbxSceneBuilder
|
||||
{
|
||||
bool BuildSceneMeshFromAssImpMesh(aiNode* currentNode, const aiScene* scene, const FbxSceneSystem& sceneSystem, AZStd::vector<AZStd::shared_ptr<DataTypes::IGraphObject>>& meshes,
|
||||
bool BuildSceneMeshFromAssImpMesh(const aiNode* currentNode, const aiScene* scene, const FbxSceneSystem& sceneSystem, AZStd::vector<AZStd::shared_ptr<DataTypes::IGraphObject>>& meshes,
|
||||
const AZStd::function<AZStd::shared_ptr<SceneData::GraphData::MeshData>()>& makeMeshFunc);
|
||||
|
||||
typedef AZ::Outcome<const SceneData::GraphData::MeshData* const, Events::ProcessingResult> GetMeshDataFromParentResult;
|
||||
GetMeshDataFromParentResult GetMeshDataFromParent(AssImpSceneNodeAppendedContext& context);
|
||||
|
||||
// If a node in the original scene file has a mesh with multiple materials on it, the associated AssImp
|
||||
// node will have multiple meshes on it, broken apart per material. This returns the total number
|
||||
// of vertices on all meshes on the given node.
|
||||
uint64_t GetVertexCountForAllMeshesOnNode(const aiNode& node, const aiScene& scene);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ namespace AZ
|
||||
Containers::SceneGraph::NodeIndex parentNode, const char* defaultName)
|
||||
{
|
||||
AZ_TraceContext("Node name", name);
|
||||
const AZStd::string originalNodeName(name);
|
||||
|
||||
bool isNameUpdated = false;
|
||||
// Nodes can't have an empty name, except of the root, otherwise nodes can't be referenced.
|
||||
@@ -56,7 +57,7 @@ namespace AZ
|
||||
// can't reference the same parent in that case. This is to make sure the node can be quickly found as
|
||||
// the full path will be unique. To fix any issues, an index is appended.
|
||||
size_t index = 1;
|
||||
size_t offset = name.length();
|
||||
const size_t offset = name.length();
|
||||
while (graph.Find(parentNode, name).IsValid())
|
||||
{
|
||||
// Remove the previously tried extension.
|
||||
@@ -71,7 +72,8 @@ namespace AZ
|
||||
if (isNameUpdated)
|
||||
{
|
||||
AZ_TraceContext("New node name", name);
|
||||
AZ_TracePrintf(Utilities::WarningWindow, "The name of the node was invalid or conflicting and was updated.");
|
||||
AZ_TracePrintf(Utilities::WarningWindow, "The name of the node '%s' was invalid or conflicting and was updated to '%s'.",
|
||||
originalNodeName.c_str(), name.c_str());
|
||||
}
|
||||
|
||||
return isNameUpdated;
|
||||
|
||||
@@ -38,12 +38,14 @@ namespace AZ
|
||||
{
|
||||
AZ_TracePrintf(SceneAPI::Utilities::LogWindow, "AssImpSceneWrapper::LoadSceneFromFile %s", fileName);
|
||||
AZ_TraceContext("Filename", fileName);
|
||||
// aiProcess_JoinIdenticalVertices is not enabled because O3DE has a mesh optimizer that also does this,
|
||||
// this flag is disabled to keep AssImp output similar to FBX SDK to reduce downstream bugs for the initial AssImp release.
|
||||
// There's currently a minimum of properties and flags set to maximize compatibility with the existing node graph.
|
||||
m_importer.SetPropertyBool(AI_CONFIG_IMPORT_FBX_PRESERVE_PIVOTS, false);
|
||||
m_importer.SetPropertyBool(AI_CONFIG_IMPORT_FBX_OPTIMIZE_EMPTY_ANIMATION_CURVES, false);
|
||||
m_sceneFileName = fileName;
|
||||
m_assImpScene = m_importer.ReadFile(fileName,
|
||||
aiProcess_Triangulate //Triangulates all faces of all meshes
|
||||
| aiProcess_JoinIdenticalVertices //Identifies and joins identical vertex data sets for the imported meshes
|
||||
| aiProcess_LimitBoneWeights //Limits the number of bones that can affect a vertex to a maximum value
|
||||
//dropping the least important and re-normalizing
|
||||
| aiProcess_GenNormals); //Generate normals for meshes
|
||||
|
||||
@@ -19,6 +19,16 @@ namespace AZ::SceneAPI::Utilities
|
||||
m_output += AZStd::string::format("\t%s: %s\n", name, data);
|
||||
}
|
||||
|
||||
void DebugOutput::WriteArray(const char* name, const unsigned int* data, int size)
|
||||
{
|
||||
m_output += AZStd::string::format("\t%s: ", name);
|
||||
for (int index = 0; index < size; ++index)
|
||||
{
|
||||
m_output += AZStd::string::format("%d, ", data[index]);
|
||||
}
|
||||
m_output += AZStd::string::format("\n");
|
||||
}
|
||||
|
||||
void DebugOutput::Write(const char* name, const AZStd::string& data)
|
||||
{
|
||||
Write(name, data.c_str());
|
||||
|
||||
@@ -29,6 +29,7 @@ namespace AZ::SceneAPI::Utilities
|
||||
void Write(const char* name, const AZStd::vector<AZStd::vector<T>>& data);
|
||||
|
||||
SCENE_CORE_API void Write(const char* name, const char* data);
|
||||
SCENE_CORE_API void WriteArray(const char* name, const unsigned int* data, int size);
|
||||
SCENE_CORE_API void Write(const char* name, const AZStd::string& data);
|
||||
SCENE_CORE_API void Write(const char* name, double data);
|
||||
SCENE_CORE_API void Write(const char* name, uint64_t data);
|
||||
|
||||
@@ -285,8 +285,26 @@ namespace AZ
|
||||
void BlendShapeData::GetDebugOutput(SceneAPI::Utilities::DebugOutput& output) const
|
||||
{
|
||||
output.Write("Positions", m_positions);
|
||||
int index = 0;
|
||||
for (const auto& position : m_positions)
|
||||
{
|
||||
output.Write(AZStd::string::format("\t%d", index).c_str(), position);
|
||||
++index;
|
||||
}
|
||||
index = 0;
|
||||
output.Write("Normals", m_normals);
|
||||
for (const auto& normal : m_normals)
|
||||
{
|
||||
output.Write(AZStd::string::format("\t%d", index).c_str(), normal);
|
||||
++index;
|
||||
}
|
||||
index = 0;
|
||||
output.Write("Faces", m_faces);
|
||||
for (const auto& face : m_faces)
|
||||
{
|
||||
output.WriteArray(AZStd::string::format("\t%d", index).c_str(), face.vertexIndex, 3);
|
||||
++index;
|
||||
}
|
||||
}
|
||||
} // GraphData
|
||||
} // SceneData
|
||||
|
||||
@@ -45,7 +45,6 @@ namespace AZ
|
||||
behaviorContext->Class<MeshData>()
|
||||
->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Common)
|
||||
->Attribute(AZ::Script::Attributes::Module, "scene")
|
||||
->Method("GetSdkMeshIndex", &MeshData::GetSdkMeshIndex)
|
||||
->Method("GetControlPointIndex", &MeshData::GetControlPointIndex)
|
||||
->Method("GetUsedControlPointCount", &MeshData::GetUsedControlPointCount)
|
||||
->Method("GetUsedPointIndexForControlPoint", &MeshData::GetUsedPointIndexForControlPoint)
|
||||
@@ -77,10 +76,6 @@ namespace AZ
|
||||
void MeshData::CloneAttributesFrom(const IGraphObject* sourceObject)
|
||||
{
|
||||
IMeshData::CloneAttributesFrom(sourceObject);
|
||||
if (const auto* typedSource = azrtti_cast<const MeshData*>(sourceObject))
|
||||
{
|
||||
SetSdkMeshIndex(typedSource->GetSdkMeshIndex());
|
||||
}
|
||||
}
|
||||
|
||||
void MeshData::AddPosition(const AZ::Vector3& position)
|
||||
@@ -111,15 +106,6 @@ namespace AZ
|
||||
m_faceMaterialIds.push_back(faceMaterialId);
|
||||
}
|
||||
|
||||
void MeshData::SetSdkMeshIndex(int sdkMeshIndex)
|
||||
{
|
||||
m_sdkMeshIndex = sdkMeshIndex;
|
||||
}
|
||||
int MeshData::GetSdkMeshIndex() const
|
||||
{
|
||||
return m_sdkMeshIndex;
|
||||
}
|
||||
|
||||
void MeshData::SetVertexIndexToControlPointIndexMap(int vertexIndex, int controlPointIndex)
|
||||
{
|
||||
m_vertexIndexToControlPointIndexMap[vertexIndex] = controlPointIndex;
|
||||
@@ -206,8 +192,26 @@ namespace AZ
|
||||
void MeshData::GetDebugOutput(SceneAPI::Utilities::DebugOutput& output) const
|
||||
{
|
||||
output.Write("Positions", m_positions);
|
||||
int index = 0;
|
||||
for (const auto& position : m_positions)
|
||||
{
|
||||
output.Write(AZStd::string::format("\t%d", index).c_str(), position);
|
||||
++index;
|
||||
}
|
||||
index = 0;
|
||||
output.Write("Normals", m_normals);
|
||||
for (const auto& normal : m_normals)
|
||||
{
|
||||
output.Write(AZStd::string::format("\t%d", index).c_str(), normal);
|
||||
++index;
|
||||
}
|
||||
index = 0;
|
||||
output.Write("FaceList", m_faceList);
|
||||
for (const auto& face : m_faceList)
|
||||
{
|
||||
output.WriteArray(AZStd::string::format("\t%d", index).c_str(), face.vertexIndex, 3);
|
||||
++index;
|
||||
}
|
||||
output.Write("FaceMaterialIds", m_faceMaterialIds);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,9 +49,6 @@ namespace AZ
|
||||
SCENE_DATA_API void AddFace(const AZ::SceneAPI::DataTypes::IMeshData::Face& face,
|
||||
unsigned int faceMaterialId = AZ::SceneAPI::DataTypes::IMeshData::s_invalidMaterialId);
|
||||
|
||||
SCENE_DATA_API void SetSdkMeshIndex(int sdkMeshIndex);
|
||||
SCENE_DATA_API int GetSdkMeshIndex() const;
|
||||
|
||||
SCENE_DATA_API void SetVertexIndexToControlPointIndexMap(int vertexIndex, int controlPointIndex);
|
||||
SCENE_DATA_API size_t GetUsedControlPointCount() const override;
|
||||
SCENE_DATA_API int GetControlPointIndex(int vertexIndex) const override;
|
||||
@@ -80,8 +77,6 @@ namespace AZ
|
||||
|
||||
AZStd::unordered_map<int, int> m_vertexIndexToControlPointIndexMap;
|
||||
AZStd::unordered_map<int, int> m_controlPointToUsedVertexIndexMap;
|
||||
|
||||
int m_sdkMeshIndex = -1;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +57,6 @@ namespace AZ
|
||||
meshData->AddNormal(Vector3{0.1f, 0.2f, 0.3f});
|
||||
meshData->AddNormal(Vector3{0.4f, 0.5f, 0.6f});
|
||||
meshData->SetOriginalUnitSizeInMeters(10.0f);
|
||||
meshData->SetSdkMeshIndex(1337);
|
||||
meshData->SetUnitSizeInMeters(0.5f);
|
||||
meshData->SetVertexIndexToControlPointIndexMap(0, 10);
|
||||
meshData->SetVertexIndexToControlPointIndexMap(1, 11);
|
||||
@@ -252,7 +251,6 @@ namespace AZ
|
||||
ExpectExecute("TestExpectFloatEquals(meshData:GetNormal(1).z, 0.6)");
|
||||
ExpectExecute("TestExpectFloatEquals(meshData:GetOriginalUnitSizeInMeters(), 10.0)");
|
||||
ExpectExecute("TestExpectFloatEquals(meshData:GetUnitSizeInMeters(), 0.5)");
|
||||
ExpectExecute("TestExpectIntegerEquals(meshData:GetSdkMeshIndex(), 1337)");
|
||||
ExpectExecute("TestExpectIntegerEquals(meshData:GetUsedControlPointCount(), 4)");
|
||||
ExpectExecute("TestExpectIntegerEquals(meshData:GetControlPointIndex(0), 10)");
|
||||
ExpectExecute("TestExpectIntegerEquals(meshData:GetControlPointIndex(1), 11)");
|
||||
|
||||
@@ -101,7 +101,7 @@ struct VSOutput
|
||||
float2 m_uv[UvSetCount] : UV1;
|
||||
float2 m_detailUv : UV3;
|
||||
|
||||
float4 m_blendMask : UV8;
|
||||
float4 m_wrinkleBlendFactors : UV8;
|
||||
};
|
||||
|
||||
#include <Atom/Features/Vertex/VertexHelper.azsli>
|
||||
@@ -132,11 +132,11 @@ VSOutput SkinVS(VSInput IN)
|
||||
|
||||
if(o_blendMask_isBound)
|
||||
{
|
||||
OUT.m_blendMask = IN.m_optional_blendMask;
|
||||
OUT.m_wrinkleBlendFactors = IN.m_optional_blendMask;
|
||||
}
|
||||
else
|
||||
{
|
||||
OUT.m_blendMask = float4(0,1,0,0);
|
||||
OUT.m_wrinkleBlendFactors = float4(0,0,0,0);
|
||||
}
|
||||
|
||||
VertexHelper(IN, OUT, worldPosition, false);
|
||||
@@ -214,7 +214,22 @@ PbrLightingOutput SkinPS_Common(VSOutput IN)
|
||||
|
||||
float2 normalUv = IN.m_uv[MaterialSrg::m_normalMapUvIndex];
|
||||
float detailLayerNormalFactor = MaterialSrg::m_detail_normal_factor * detailLayerBlendFactor;
|
||||
|
||||
|
||||
// ------- Wrinkle Map Setup -------
|
||||
|
||||
// Combine the optional per-morph target wrinkle masks
|
||||
float4 wrinkleBlendFactors = float4(0.0, 0.0, 0.0, 0.0);
|
||||
for(uint wrinkleMaskIndex = 0; wrinkleMaskIndex < ObjectSrg::m_wrinkle_mask_count; ++wrinkleMaskIndex)
|
||||
{
|
||||
wrinkleBlendFactors += ObjectSrg::m_wrinkle_masks[wrinkleMaskIndex].Sample(MaterialSrg::m_sampler, normalUv) * ObjectSrg::GetWrinkleMaskWeight(wrinkleMaskIndex);
|
||||
}
|
||||
|
||||
// If texture based morph target driven masks are being used, use those values instead of the per-vertex colors
|
||||
if(ObjectSrg::m_wrinkle_mask_count)
|
||||
{
|
||||
IN.m_wrinkleBlendFactors = saturate(wrinkleBlendFactors);
|
||||
}
|
||||
|
||||
// Since the wrinkle normal maps should all be in the same tangent space as the main normal map, we should be able to blend the raw normal map
|
||||
// texture values before doing all the tangent space transforms, so we only have to do the transforms once, for better performance.
|
||||
|
||||
@@ -223,12 +238,12 @@ PbrLightingOutput SkinPS_Common(VSOutput IN)
|
||||
{
|
||||
normalMapSample = SampleNormalXY(MaterialSrg::m_normalMap, MaterialSrg::m_sampler, normalUv, MaterialSrg::m_flipNormalX, MaterialSrg::m_flipNormalY);
|
||||
}
|
||||
if(o_wrinkleLayers_enabled && o_blendMask_isBound && o_wrinkleLayers_normal_enabled)
|
||||
if(o_wrinkleLayers_enabled && o_wrinkleLayers_normal_enabled)
|
||||
{
|
||||
normalMapSample = ApplyNormalWrinkleMap(o_wrinkleLayers_normal_useTexture1, normalMapSample, MaterialSrg::m_wrinkle_normal_texture1, MaterialSrg::m_sampler, normalUv, MaterialSrg::m_flipNormalX, MaterialSrg::m_flipNormalY, IN.m_blendMask.r);
|
||||
normalMapSample = ApplyNormalWrinkleMap(o_wrinkleLayers_normal_useTexture2, normalMapSample, MaterialSrg::m_wrinkle_normal_texture2, MaterialSrg::m_sampler, normalUv, MaterialSrg::m_flipNormalX, MaterialSrg::m_flipNormalY, IN.m_blendMask.g);
|
||||
normalMapSample = ApplyNormalWrinkleMap(o_wrinkleLayers_normal_useTexture3, normalMapSample, MaterialSrg::m_wrinkle_normal_texture3, MaterialSrg::m_sampler, normalUv, MaterialSrg::m_flipNormalX, MaterialSrg::m_flipNormalY, IN.m_blendMask.b);
|
||||
normalMapSample = ApplyNormalWrinkleMap(o_wrinkleLayers_normal_useTexture4, normalMapSample, MaterialSrg::m_wrinkle_normal_texture4, MaterialSrg::m_sampler, normalUv, MaterialSrg::m_flipNormalX, MaterialSrg::m_flipNormalY, IN.m_blendMask.a);
|
||||
normalMapSample = ApplyNormalWrinkleMap(o_wrinkleLayers_normal_useTexture1, normalMapSample, MaterialSrg::m_wrinkle_normal_texture1, MaterialSrg::m_sampler, normalUv, MaterialSrg::m_flipNormalX, MaterialSrg::m_flipNormalY, IN.m_wrinkleBlendFactors.r);
|
||||
normalMapSample = ApplyNormalWrinkleMap(o_wrinkleLayers_normal_useTexture2, normalMapSample, MaterialSrg::m_wrinkle_normal_texture2, MaterialSrg::m_sampler, normalUv, MaterialSrg::m_flipNormalX, MaterialSrg::m_flipNormalY, IN.m_wrinkleBlendFactors.g);
|
||||
normalMapSample = ApplyNormalWrinkleMap(o_wrinkleLayers_normal_useTexture3, normalMapSample, MaterialSrg::m_wrinkle_normal_texture3, MaterialSrg::m_sampler, normalUv, MaterialSrg::m_flipNormalX, MaterialSrg::m_flipNormalY, IN.m_wrinkleBlendFactors.b);
|
||||
normalMapSample = ApplyNormalWrinkleMap(o_wrinkleLayers_normal_useTexture4, normalMapSample, MaterialSrg::m_wrinkle_normal_texture4, MaterialSrg::m_sampler, normalUv, MaterialSrg::m_flipNormalX, MaterialSrg::m_flipNormalY, IN.m_wrinkleBlendFactors.a);
|
||||
}
|
||||
|
||||
if(o_detail_normal_useTexture)
|
||||
@@ -255,7 +270,7 @@ PbrLightingOutput SkinPS_Common(VSOutput IN)
|
||||
float3 baseColor = GetBaseColorInput(MaterialSrg::m_baseColorMap, MaterialSrg::m_sampler, baseColorUv, MaterialSrg::m_baseColor, o_baseColor_useTexture);
|
||||
|
||||
bool useSampledBaseColor = o_baseColor_useTexture;
|
||||
if(o_wrinkleLayers_enabled && o_blendMask_isBound && o_wrinkleLayers_baseColor_enabled)
|
||||
if(o_wrinkleLayers_enabled && o_wrinkleLayers_baseColor_enabled)
|
||||
{
|
||||
// If any of the wrinkle maps are applied, we will use the Base Color blend settings to apply the MaterialSrg::m_baseColor tint to the wrinkle maps,
|
||||
// even if the main base color map is not used.
|
||||
@@ -272,10 +287,10 @@ PbrLightingOutput SkinPS_Common(VSOutput IN)
|
||||
baseColor = float3(1,1,1);
|
||||
}
|
||||
|
||||
baseColor = ApplyBaseColorWrinkleMap(o_wrinkleLayers_baseColor_useTexture1, baseColor, MaterialSrg::m_wrinkle_baseColor_texture1, MaterialSrg::m_sampler, baseColorUv, IN.m_blendMask.r);
|
||||
baseColor = ApplyBaseColorWrinkleMap(o_wrinkleLayers_baseColor_useTexture2, baseColor, MaterialSrg::m_wrinkle_baseColor_texture2, MaterialSrg::m_sampler, baseColorUv, IN.m_blendMask.g);
|
||||
baseColor = ApplyBaseColorWrinkleMap(o_wrinkleLayers_baseColor_useTexture3, baseColor, MaterialSrg::m_wrinkle_baseColor_texture3, MaterialSrg::m_sampler, baseColorUv, IN.m_blendMask.b);
|
||||
baseColor = ApplyBaseColorWrinkleMap(o_wrinkleLayers_baseColor_useTexture4, baseColor, MaterialSrg::m_wrinkle_baseColor_texture4, MaterialSrg::m_sampler, baseColorUv, IN.m_blendMask.a);
|
||||
baseColor = ApplyBaseColorWrinkleMap(o_wrinkleLayers_baseColor_useTexture1, baseColor, MaterialSrg::m_wrinkle_baseColor_texture1, MaterialSrg::m_sampler, baseColorUv, IN.m_wrinkleBlendFactors.r);
|
||||
baseColor = ApplyBaseColorWrinkleMap(o_wrinkleLayers_baseColor_useTexture2, baseColor, MaterialSrg::m_wrinkle_baseColor_texture2, MaterialSrg::m_sampler, baseColorUv, IN.m_wrinkleBlendFactors.g);
|
||||
baseColor = ApplyBaseColorWrinkleMap(o_wrinkleLayers_baseColor_useTexture3, baseColor, MaterialSrg::m_wrinkle_baseColor_texture3, MaterialSrg::m_sampler, baseColorUv, IN.m_wrinkleBlendFactors.b);
|
||||
baseColor = ApplyBaseColorWrinkleMap(o_wrinkleLayers_baseColor_useTexture4, baseColor, MaterialSrg::m_wrinkle_baseColor_texture4, MaterialSrg::m_sampler, baseColorUv, IN.m_wrinkleBlendFactors.a);
|
||||
|
||||
}
|
||||
|
||||
@@ -283,13 +298,13 @@ PbrLightingOutput SkinPS_Common(VSOutput IN)
|
||||
|
||||
baseColor = ApplyTextureOverlay(o_detail_baseColor_useTexture, baseColor, MaterialSrg::m_detail_baseColor_texture, MaterialSrg::m_sampler, IN.m_detailUv, detailLayerBaseColorFactor);
|
||||
|
||||
if(o_wrinkleLayers_enabled && o_wrinkleLayers_showBlendMaskValues && o_blendMask_isBound)
|
||||
if(o_wrinkleLayers_enabled && o_wrinkleLayers_showBlendMaskValues)
|
||||
{
|
||||
// Overlay debug colors to highlight the different blend weights coming from the vertex color stream.
|
||||
if(o_wrinkleLayers_count > 0) { baseColor = lerp(baseColor, float3(1,0,0), IN.m_blendMask.r); }
|
||||
if(o_wrinkleLayers_count > 1) { baseColor = lerp(baseColor, float3(0,1,0), IN.m_blendMask.g); }
|
||||
if(o_wrinkleLayers_count > 2) { baseColor = lerp(baseColor, float3(0,0,1), IN.m_blendMask.b); }
|
||||
if(o_wrinkleLayers_count > 3) { baseColor = lerp(baseColor, float3(1,1,1), IN.m_blendMask.a); }
|
||||
if(o_wrinkleLayers_count > 0) { baseColor = lerp(baseColor, float3(1,0,0), IN.m_wrinkleBlendFactors.r); }
|
||||
if(o_wrinkleLayers_count > 1) { baseColor = lerp(baseColor, float3(0,1,0), IN.m_wrinkleBlendFactors.g); }
|
||||
if(o_wrinkleLayers_count > 2) { baseColor = lerp(baseColor, float3(0,0,1), IN.m_wrinkleBlendFactors.b); }
|
||||
if(o_wrinkleLayers_count > 3) { baseColor = lerp(baseColor, float3(1,1,1), IN.m_wrinkleBlendFactors.a); }
|
||||
}
|
||||
|
||||
// ------- Specular -------
|
||||
|
||||
@@ -987,15 +987,6 @@
|
||||
{
|
||||
"file": "Shaders/MotionVector/SkinnedMeshMotionVector.shader",
|
||||
"tag": "SkinnedMeshMotionVector"
|
||||
},
|
||||
// Used by the light culling system to produce accurate depth bounds for this object when it uses blended transparency
|
||||
{
|
||||
"file": "Shaders/Depth/DepthPassTransparentMin.shader",
|
||||
"tag": "DepthPassTransparentMin"
|
||||
},
|
||||
{
|
||||
"file": "Shaders/Depth/DepthPassTransparentMax.shader",
|
||||
"tag": "DepthPassTransparentMax"
|
||||
}
|
||||
],
|
||||
"functors": [
|
||||
|
||||
@@ -1199,6 +1199,14 @@
|
||||
"file": "./StandardPBR_ForwardPass_EDS.shader",
|
||||
"tag": "ForwardPass_EDS"
|
||||
},
|
||||
{
|
||||
"file": "./StandardPBR_LowEndForward.shader",
|
||||
"tag": "LowEndForward"
|
||||
},
|
||||
{
|
||||
"file": "./StandardPBR_LowEndForward_EDS.shader",
|
||||
"tag": "LowEndForward_EDS"
|
||||
},
|
||||
{
|
||||
"file": "Shaders/Shadow/Shadowmap.shader",
|
||||
"tag": "Shadowmap"
|
||||
@@ -1289,10 +1297,6 @@
|
||||
"textureProperty": "baseColor.textureMap",
|
||||
"useTextureProperty": "baseColor.useTexture",
|
||||
"dependentProperties": ["baseColor.textureMapUv", "baseColor.textureBlendMode"],
|
||||
"shaderTags": [
|
||||
"ForwardPass",
|
||||
"ForwardPass_EDS"
|
||||
],
|
||||
"shaderOption": "o_baseColor_useTexture"
|
||||
}
|
||||
},
|
||||
@@ -1302,10 +1306,6 @@
|
||||
"textureProperty": "metallic.textureMap",
|
||||
"useTextureProperty": "metallic.useTexture",
|
||||
"dependentProperties": ["metallic.textureMapUv"],
|
||||
"shaderTags": [
|
||||
"ForwardPass",
|
||||
"ForwardPass_EDS"
|
||||
],
|
||||
"shaderOption": "o_metallic_useTexture"
|
||||
}
|
||||
},
|
||||
@@ -1315,10 +1315,6 @@
|
||||
"textureProperty": "specularF0.textureMap",
|
||||
"useTextureProperty": "specularF0.useTexture",
|
||||
"dependentProperties": ["specularF0.textureMapUv"],
|
||||
"shaderTags": [
|
||||
"ForwardPass",
|
||||
"ForwardPass_EDS"
|
||||
],
|
||||
"shaderOption": "o_specularF0_useTexture"
|
||||
}
|
||||
},
|
||||
@@ -1328,10 +1324,6 @@
|
||||
"textureProperty": "normal.textureMap",
|
||||
"useTextureProperty": "normal.useTexture",
|
||||
"dependentProperties": ["normal.textureMapUv", "normal.factor", "normal.flipX", "normal.flipY"],
|
||||
"shaderTags": [
|
||||
"ForwardPass",
|
||||
"ForwardPass_EDS"
|
||||
],
|
||||
"shaderOption": "o_normal_useTexture"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
*
|
||||
*/
|
||||
|
||||
#include "Atom/Features/ShaderQualityOptions.azsli"
|
||||
|
||||
#include "StandardPBR_Common.azsli"
|
||||
|
||||
// SRGs
|
||||
@@ -317,13 +319,18 @@ ForwardPassOutputWithDepth StandardPbr_ForwardPassPS(VSOutput IN, bool isFrontFa
|
||||
|
||||
PbrLightingOutput lightingOutput = ForwardPassPS_Common(IN, isFrontFace, depth);
|
||||
|
||||
#ifdef UNIFIED_FORWARD_OUTPUT
|
||||
OUT.m_color.rgb = lightingOutput.m_diffuseColor.rgb + lightingOutput.m_specularColor.rgb;
|
||||
OUT.m_color.a = lightingOutput.m_diffuseColor.a;
|
||||
OUT.m_depth = depth;
|
||||
#else
|
||||
OUT.m_diffuseColor = lightingOutput.m_diffuseColor;
|
||||
OUT.m_specularColor = lightingOutput.m_specularColor;
|
||||
OUT.m_specularF0 = lightingOutput.m_specularF0;
|
||||
OUT.m_albedo = lightingOutput.m_albedo;
|
||||
OUT.m_normal = lightingOutput.m_normal;
|
||||
OUT.m_depth = depth;
|
||||
|
||||
#endif
|
||||
return OUT;
|
||||
}
|
||||
|
||||
@@ -335,12 +342,16 @@ ForwardPassOutput StandardPbr_ForwardPassPS_EDS(VSOutput IN, bool isFrontFace :
|
||||
|
||||
PbrLightingOutput lightingOutput = ForwardPassPS_Common(IN, isFrontFace, depth);
|
||||
|
||||
#ifdef UNIFIED_FORWARD_OUTPUT
|
||||
OUT.m_color.rgb = lightingOutput.m_diffuseColor.rgb + lightingOutput.m_specularColor.rgb;
|
||||
OUT.m_color.a = lightingOutput.m_diffuseColor.a;
|
||||
#else
|
||||
OUT.m_diffuseColor = lightingOutput.m_diffuseColor;
|
||||
OUT.m_specularColor = lightingOutput.m_specularColor;
|
||||
OUT.m_specularF0 = lightingOutput.m_specularF0;
|
||||
OUT.m_albedo = lightingOutput.m_albedo;
|
||||
OUT.m_normal = lightingOutput.m_normal;
|
||||
|
||||
#endif
|
||||
return OUT;
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user