Merge remote-tracking branch 'upstream/stabilization/2106' into santorac/stabilization/2106/MissingDependencyWarnings-ATOM-15136

main
Chris Santora 5 years ago
commit 71525ce601

@ -1,34 +0,0 @@
CryEngine tips of the day
You can toggle snap to grid by pressing G.
Ctrl+Shift+Clicking somewhere with an object selected quickly moves the object to that position when in move mode.
Pressing M will open the material editor.
Show and Hide helpers is bound to Shift + Space by default.
Enable AI/Physics is bound to Ctrl + P by default.
You can save a viewport location by pressing Ctrl + F1 through f12 and go to that position using Shift + F1 through F12.
You can link objects together by using the link command on the top menu of the editor.
Pressing 1 through 5 on the keyboard will cycle through brush operations such as move or scale.
You can simply bind keyboard shortcuts to editor functions by going to Tools --> Customize Keyboard.
Pressing H will hide the selected objects, Ctrl-H will unhide all hidden objects.
Pressing F will freeze the selected objects, Ctrl-F will unfreeze all frozen objects.
Pressing F3 will toggle wireframe view.
Camera/terrain collision can be toggled using Q.
You can restart the Editor by pressing the restart button on your PC.
Pressing Ctrl-C with an object selected will clone that object.
Toggle the console by pressing the tilde (~) key.
You can dock windows by dragging them onto the blue helpers that appear when you grab a window by the titlebar.
You can select materials by clicking on the dropper icon in the material editor and then clicking on the material you wish to select.
You can right click on the previewer in the material editor and change the model to different shapes and background colors.
Materials can be saved in the local level folder for re-distribution.
Always keep your level free of errors and immidiately fix errors reported by the error report screen when you load your level.
You must always export to engine before you can run it in pure game mode. (File --> Export to engine)
You must re-triangulate AI before playing your level in game mode. (AI --> Generate all navigation)
You must always re-generate surface textures after you finish painting the terrain. (File --> Regenerate surface textures)
Press Ctrl-G or F12 to go into the Game mode, ESC to return to Editing mode.
Quickly rebuild a level (without regenerating the ground texture) by pressing Ctrl-E.
Hold down the third mouse button and drag to move the camera up and down.
Missing objects are represented by a bright yellow sphere.
Hold Alt + Middle Mouse button to rotate around an object.
Select multiple objects by holding Ctrl.
You can place multiple instances of vegetation by holding Shift and clicking on the terrain.
A number of useful commands can be found in Tools --> User commands. This can also be dragged and docked to the main window.

@ -14679,6 +14679,25 @@ An Entity can be selected by using the pick button, or by dragging an Entity fro
<translation></translation> <translation></translation>
</message> </message>
</context> </context>
<context>
<name>Method: NetBindComponent</name>
<message id="NETBINDCOMPONENT_ISNETENTITYROLEAUTHORITY_TOOLTIP">
<source>NETBINDCOMPONENT_ISNETENTITYROLEAUTHORITY_TOOLTIP</source>
<translation>Returns true if this network entity is an authoritative proxy on a server (full authority); otherwise false.</translation>
</message>
<message id="NETBINDCOMPONENT_ISNETENTITYROLEAUTONOMOUS_TOOLTIP">
<source>NETBINDCOMPONENT_ISNETENTITYROLEAUTONOMOUS_TOOLTIP</source>
<translation>Returns true if this network entity is an autonomous proxy on a client (can execute local prediction) or if this network entity is an authoritative proxy on a server but has autonomous privileges (ie: a host who is also a player); otherwise false.</translation>
</message>
<message id="NETBINDCOMPONENT_ISNETENTITYROLECLIENT_TOOLTIP">
<source>NETBINDCOMPONENT_ISNETENTITYROLECLIENT_TOOLTIP</source>
<translation>Returns true if this network entity is a simulated proxy on a client; otherwise false.</translation>
</message>
<message id="NETBINDCOMPONENT_ISNETENTITYROLESERVER_TOOLTIP">
<source>NETBINDCOMPONENT_ISNETENTITYROLESERVER_TOOLTIP</source>
<translation>Returns true if this network entity is a simulated proxy on a server (ie: a different server may own this entity, but the entity has been replicated to this server; otherwise false.</translation>
</message>
</context>
<context> <context>
<name>Method: Math</name> <name>Method: Math</name>
<message id="MATH_NAME"> <message id="MATH_NAME">

@ -1,38 +0,0 @@
# -*- coding: utf-8 -*-
#
# 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
os.chdir('qml')
startDir = os.getcwd()
# since it's a .exe file it will only work on windows, but we may as well
# construct the path in a platform-independent way.
lreleaseCmd = os.path.join(startDir, '..', '..', '..',
'Code', 'SDKs', 'Qt', 'x64', 'bin', 'lrelease.exe ')
print(startDir)
# Korean, Japanese and Simplified Chinese
targetLanguages = ['ko', 'ja', 'zh_CN']
for lang in targetLanguages:
os.chdir(startDir)
tgtLang = '-target-language ' + lang
os.system(lreleaseCmd + 'this_' + lang + '.ts')
for fileName in os.listdir():
if not fileName.endswith(".ts"):
continue
os.system(lreleaseCmd + ' ' + fileName)
print(('Finished processing: ' + fileName))

@ -112,7 +112,7 @@ def remove_file(file_path: str) -> None:
@pytest.mark.parametrize('project', ['AutomatedTesting']) @pytest.mark.parametrize('project', ['AutomatedTesting'])
@pytest.mark.parametrize('level', ['AWS/Metrics']) @pytest.mark.parametrize('level', ['AWS/Metrics'])
@pytest.mark.parametrize('feature_name', [AWS_METRICS_FEATURE_NAME]) @pytest.mark.parametrize('feature_name', [AWS_METRICS_FEATURE_NAME])
@pytest.mark.parametrize('resource_mappings_filename', ['aws_resource_mappings.json']) @pytest.mark.parametrize('resource_mappings_filename', ['default_aws_resource_mappings.json'])
@pytest.mark.parametrize('profile_name', ['AWSAutomationTest']) @pytest.mark.parametrize('profile_name', ['AWSAutomationTest'])
@pytest.mark.parametrize('region_name', ['us-west-2']) @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('assume_role_arn', ['arn:aws:iam::645075835648:role/o3de-automation-tests'])

@ -36,7 +36,7 @@ logger = logging.getLogger(__name__)
@pytest.mark.usefixtures('cdk') @pytest.mark.usefixtures('cdk')
@pytest.mark.parametrize('feature_name', [AWS_CLIENT_AUTH_FEATURE_NAME]) @pytest.mark.parametrize('feature_name', [AWS_CLIENT_AUTH_FEATURE_NAME])
@pytest.mark.usefixtures('resource_mappings') @pytest.mark.usefixtures('resource_mappings')
@pytest.mark.parametrize('resource_mappings_filename', ['aws_resource_mappings.json']) @pytest.mark.parametrize('resource_mappings_filename', ['default_aws_resource_mappings.json'])
@pytest.mark.usefixtures('aws_utils') @pytest.mark.usefixtures('aws_utils')
@pytest.mark.parametrize('region_name', ['us-west-2']) @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('assume_role_arn', ['arn:aws:iam::645075835648:role/o3de-automation-tests'])

@ -35,7 +35,7 @@ logger = logging.getLogger(__name__)
@pytest.mark.usefixtures('cdk') @pytest.mark.usefixtures('cdk')
@pytest.mark.parametrize('feature_name', [AWS_CLIENT_AUTH_FEATURE_NAME]) @pytest.mark.parametrize('feature_name', [AWS_CLIENT_AUTH_FEATURE_NAME])
@pytest.mark.usefixtures('resource_mappings') @pytest.mark.usefixtures('resource_mappings')
@pytest.mark.parametrize('resource_mappings_filename', ['aws_resource_mappings.json']) @pytest.mark.parametrize('resource_mappings_filename', ['default_aws_resource_mappings.json'])
@pytest.mark.usefixtures('aws_utils') @pytest.mark.usefixtures('aws_utils')
@pytest.mark.parametrize('region_name', ['us-west-2']) @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('assume_role_arn', ['arn:aws:iam::645075835648:role/o3de-automation-tests'])

@ -15,7 +15,7 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED AND PAL_TRAIT_BUILD_HOST_TOOLS AND PAL_TRAIT_
TEST_SUITE main TEST_SUITE main
TEST_SERIAL TEST_SERIAL
PATH ${CMAKE_CURRENT_LIST_DIR} PATH ${CMAKE_CURRENT_LIST_DIR}
PYTEST_MARKS "not SUITE_sandbox and not SUITE_periodic and not SUITE_benchmark" PYTEST_MARKS "SUITE_main and not REQUIRES_gpu"
TIMEOUT 1500 TIMEOUT 1500
RUNTIME_DEPENDENCIES RUNTIME_DEPENDENCIES
Legacy::Editor Legacy::Editor
@ -30,7 +30,23 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED AND PAL_TRAIT_BUILD_HOST_TOOLS AND PAL_TRAIT_
TEST_SUITE periodic TEST_SUITE periodic
TEST_SERIAL TEST_SERIAL
PATH ${CMAKE_CURRENT_LIST_DIR} PATH ${CMAKE_CURRENT_LIST_DIR}
PYTEST_MARKS "SUITE_periodic" PYTEST_MARKS "SUITE_periodic and not REQUIRES_gpu"
TIMEOUT 1500
RUNTIME_DEPENDENCIES
Legacy::Editor
AZ::AssetProcessor
AutomatedTesting.Assets
COMPONENT
Editor
)
ly_add_pytest(
NAME AutomatedTesting::EditorTests_Main_GPU
TEST_SUITE main
TEST_SERIAL
TEST_REQUIRES gpu
PATH ${CMAKE_CURRENT_LIST_DIR}
PYTEST_MARKS "SUITE_main and REQUIRES_gpu"
TIMEOUT 1500 TIMEOUT 1500
RUNTIME_DEPENDENCIES RUNTIME_DEPENDENCIES
Legacy::Editor Legacy::Editor

@ -79,8 +79,6 @@ class TestBasicEditorWorkflows(EditorTestHelper):
grp_box = new_level_dlg.findChild(QtWidgets.QGroupBox, "STATIC_GROUP1") grp_box = new_level_dlg.findChild(QtWidgets.QGroupBox, "STATIC_GROUP1")
level_name = grp_box.findChild(QtWidgets.QLineEdit, "LEVEL") level_name = grp_box.findChild(QtWidgets.QLineEdit, "LEVEL")
level_name.setText(self.args["level"]) level_name.setText(self.args["level"])
level_folders = grp_box.findChild(QtWidgets.QComboBox, "LEVEL_FOLDERS")
level_folders.setCurrentText("Levels/")
button_box = new_level_dlg.findChild(QtWidgets.QDialogButtonBox, "buttonBox") button_box = new_level_dlg.findChild(QtWidgets.QDialogButtonBox, "buttonBox")
button_box.button(QtWidgets.QDialogButtonBox.Ok).click() button_box.button(QtWidgets.QDialogButtonBox.Ok).click()

@ -66,3 +66,35 @@ class TestBasicEditorWorkflows(object):
timeout=log_monitor_timeout, timeout=log_monitor_timeout,
auto_test_mode=False auto_test_mode=False
) )
@pytest.mark.test_case_id("C6351273", "C6384955", "C16929880", "C15167490", "C15167491")
@pytest.mark.SUITE_main
@pytest.mark.REQUIRES_gpu
def test_BasicEditorWorkflows_GPU_LevelEntityComponentCRUD(self, request, editor, level, launcher_platform):
# Skip test if running against Debug build
if "debug" in internal_plugin.build_directory:
pytest.skip("Does not execute against debug builds.")
expected_lines = [
"Create and load new level: True",
"New entity creation: True",
"Create entity hierarchy: True",
"Add component: True",
"Component update: True",
"Remove component: True",
"Save and Export: True",
"BasicEditorWorkflows_LevelEntityComponent: result=SUCCESS",
]
hydra.launch_and_validate_results(
request,
test_directory,
editor,
"BasicEditorWorkflows_LevelEntityComponentCRUD.py",
expected_lines,
cfg_args=[level],
timeout=log_monitor_timeout,
auto_test_mode=False,
null_renderer=False
)

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

@ -57,7 +57,13 @@ namespace AZ
//! Determines if a component can be created from the asset type //! Determines if a component can be created from the asset type
//! This will be called before attempting to create a component from an asset (drag&drop, etc) //! This will be called before attempting to create a component from an asset (drag&drop, etc)
//! You can use this to filter by subIds or do your own validation here if needed //! You can use this to filter by subIds or do your own validation here if needed
virtual bool CanCreateComponent(const AZ::Data::AssetId& /*assetId*/) const { return true; } virtual bool CanCreateComponent([[maybe_unused]] const AZ::Data::AssetId& assetId) const { return true; }
//! Determines if other products conflict with the given one when multiple are generated from a source asset.
//! This will be called before attempting to create a component from an asset (drag&drop, etc)
//! You can use this to filter by conflicting product types or in case you want to skip for UX reasons.
//! @param[in] productAssetTypes Asset types of all generated products, including the one for our given type in this bus.
virtual bool HasConflictingProducts([[maybe_unused]] const AZStd::vector<AZ::Data::AssetType>& productAssetTypes) const { return false; }
}; };
using AssetTypeInfoBus = AZ::EBus<AssetTypeInfo>; using AssetTypeInfoBus = AZ::EBus<AssetTypeInfo>;

@ -44,8 +44,7 @@ namespace AZ::IO
//! make adjustments. For the most optimal performance align read buffers to the physicalSectorSize. //! make adjustments. For the most optimal performance align read buffers to the physicalSectorSize.
u8 m_enableUnbufferedReads : 1; u8 m_enableUnbufferedReads : 1;
//! Globally enable file sharing. This allows files to used outside AZ::IO::Streamer, including other applications //! Globally enable file sharing. This allows files to used outside AZ::IO::Streamer, including other applications
//! while in use by AZ::IO::Streamer. File sharing can negatively impact performance and is recommended for //! while in use by AZ::IO::Streamer.
//! development only.
u8 m_enableSharing : 1; u8 m_enableSharing : 1;
//! If true, only information that's explicitly requested or issues are reported. If false, status information //! If true, only information that's explicitly requested or issues are reported. If false, status information
//! such as when drives are created and destroyed is reported as well. //! such as when drives are created and destroyed is reported as well.

@ -312,7 +312,7 @@ namespace AZ::IO
{ {
if (reportHardware) if (reportHardware)
{ {
AZ_Printf("Streamer", "Skipping drive '%s' because to no paths make use of it.\n", driveIt); AZ_Printf("Streamer", "Skipping drive '%s' because no paths make use of it.\n", driveIt);
} }
while (*driveIt++); while (*driveIt++);
continue; continue;

@ -46,6 +46,7 @@ namespace AZ::IO
options.m_hasSeekPenalty = HasSeekPenalty; options.m_hasSeekPenalty = HasSeekPenalty;
options.m_enableUnbufferedReads = TestEnableUnbufferReads; options.m_enableUnbufferedReads = TestEnableUnbufferReads;
options.m_enableSharing = TestEnableSharedReads; options.m_enableSharing = TestEnableSharedReads;
options.m_minimalReporting = true;
return StorageDriveWin({ "c:/" }, TestMaxFileHandles, TestMaxMetaDataEntries, TestPhysicalSectorSize, return StorageDriveWin({ "c:/" }, TestMaxFileHandles, TestMaxMetaDataEntries, TestPhysicalSectorSize,
TestLogicalSectorSize, TestMaxIOChannels, TestOverCommit, options); TestLogicalSectorSize, TestMaxIOChannels, TestOverCommit, options);
@ -151,6 +152,7 @@ namespace AZ::IO
m_configurationOptions.m_hasSeekPenalty = HasSeekPenalty; m_configurationOptions.m_hasSeekPenalty = HasSeekPenalty;
m_configurationOptions.m_enableUnbufferedReads = TestEnableUnbufferReads; m_configurationOptions.m_enableUnbufferedReads = TestEnableUnbufferReads;
m_configurationOptions.m_enableSharing = TestEnableSharedReads; m_configurationOptions.m_enableSharing = TestEnableSharedReads;
m_configurationOptions.m_minimalReporting = true;
m_storageDriveWin = AZStd::make_shared<AZ::IO::StorageDriveWin>(AZStd::vector<AZStd::string_view>{drive}, TestMaxFileHandles, m_storageDriveWin = AZStd::make_shared<AZ::IO::StorageDriveWin>(AZStd::vector<AZStd::string_view>{drive}, TestMaxFileHandles,
TestMaxMetaDataEntries, TestPhysicalSectorSize, TestLogicalSectorSize, TestMaxIOChannels, overCommit, m_configurationOptions); TestMaxMetaDataEntries, TestPhysicalSectorSize, TestLogicalSectorSize, TestMaxIOChannels, overCommit, m_configurationOptions);
@ -1148,3 +1150,142 @@ namespace AZ::IO
azfree(buffers[numRequests - 1]); azfree(buffers[numRequests - 1]);
} }
} // namespace AZ::IO } // namespace AZ::IO
#if defined(HAVE_BENCHMARK)
#include <benchmark/benchmark.h>
namespace Benchmark
{
class StorageDriveWindowsFixture : public benchmark::Fixture
{
public:
constexpr static char* TestFileName = "StreamerBenchmark.bin";
constexpr static size_t FileSize = 64_mib;
void SetupStreamer(bool enableFileSharing)
{
using namespace AZ::IO;
m_fileIO = new UnitTest::TestFileIOBase();
m_previousFileIO = AZ::IO::FileIOBase::GetInstance();
AZ::IO::FileIOBase::SetInstance(nullptr);
AZ::IO::FileIOBase::SetInstance(m_fileIO);
SystemFile file;
file.Open(TestFileName, SystemFile::OpenMode::SF_OPEN_CREATE | SystemFile::OpenMode::SF_OPEN_READ_WRITE);
AZStd::unique_ptr<char[]> buffer(new char[FileSize]);
::memset(buffer.get(), 'c', FileSize);
file.Write(buffer.get(), FileSize);
file.Close();
AZStd::optional<AZ::IO::FixedMaxPathString> absolutePath = AZ::Utils::ConvertToAbsolutePath(TestFileName);
if (absolutePath.has_value())
{
AZStd::string drive;
AZ::StringFunc::Path::GetDrive(absolutePath->c_str(), drive);
m_absolutePath = *absolutePath;
StorageDriveWin::ConstructionOptions options;
options.m_hasSeekPenalty = false;
options.m_enableUnbufferedReads = true; // Leave this on otherwise repeated loads will be using the Windows cache instead.
options.m_enableSharing = enableFileSharing;
options.m_minimalReporting = true;
AZStd::shared_ptr<StreamStackEntry> storageDriveWin =
AZStd::make_shared<StorageDriveWin>(AZStd::vector<AZStd::string_view>{ drive }, 32, 32, 4_kib, 512, 8, 0, options);
AZStd::unique_ptr<Scheduler> stack = AZStd::make_unique<Scheduler>(AZStd::move(storageDriveWin));
m_streamer = aznew Streamer(AZStd::thread_desc{}, AZStd::move(stack));
}
}
void TearDown([[maybe_unused]] const ::benchmark::State& state) override
{
using namespace AZ::IO;
AZStd::string temp;
m_absolutePath.swap(temp);
delete m_streamer;
SystemFile::Delete(TestFileName);
AZ::IO::FileIOBase::SetInstance(nullptr);
AZ::IO::FileIOBase::SetInstance(m_previousFileIO);
delete m_fileIO;
}
void RepeatedlyReadFile(benchmark::State& state)
{
using namespace AZ::IO;
using namespace AZStd::chrono;
AZStd::unique_ptr<char[]> buffer(new char[FileSize]);
for (auto _ : state)
{
AZStd::binary_semaphore waitForReads;
AZStd::atomic<system_clock::time_point> end;
auto callback = [&end, &waitForReads]([[maybe_unused]] FileRequestHandle request)
{
benchmark::DoNotOptimize(end = high_resolution_clock::now());
waitForReads.release();
};
FileRequestPtr request = m_streamer->Read(m_absolutePath, buffer.get(), state.range(0), state.range(0));
m_streamer->SetRequestCompleteCallback(request, callback);
system_clock::time_point start;
benchmark::DoNotOptimize(start = high_resolution_clock::now());
m_streamer->QueueRequest(request);
waitForReads.try_acquire_for(AZStd::chrono::seconds(5));
auto durationInSeconds = duration_cast<duration<double>>(end.load() - start);
state.SetIterationTime(durationInSeconds.count());
m_streamer->QueueRequest(m_streamer->FlushCaches());
}
}
AZStd::string m_absolutePath;
AZ::IO::Streamer* m_streamer{};
AZ::IO::FileIOBase* m_previousFileIO{};
UnitTest::TestFileIOBase* m_fileIO{};
};
BENCHMARK_DEFINE_F(StorageDriveWindowsFixture, ReadsBaseline)(benchmark::State& state)
{
constexpr bool EnableFileSharing = false;
SetupStreamer(EnableFileSharing);
RepeatedlyReadFile(state);
}
BENCHMARK_DEFINE_F(StorageDriveWindowsFixture, ReadsWithFileReadSharingEnabled)(benchmark::State& state)
{
using namespace AZ::IO;
constexpr bool EnableFileSharing = true;
SetupStreamer(EnableFileSharing);
RepeatedlyReadFile(state);
}
// For these benchmarks the CPU stat doesn't provide useful information because it uses GetThreadTimes on Window but since the main
// thread is mostly sleeping while waiting for the read on the Streamer thread to complete this will report values (close to) zero.
BENCHMARK_REGISTER_F(StorageDriveWindowsFixture, ReadsBaseline)
->RangeMultiplier(8)
->Range(1024, 64_mib)
->UseManualTime()
->Unit(benchmark::kMillisecond);
BENCHMARK_REGISTER_F(StorageDriveWindowsFixture, ReadsWithFileReadSharingEnabled)
->RangeMultiplier(8)
->Range(1024, 64_mib)
->UseManualTime()
->Unit(benchmark::kMillisecond);
} // namespace Benchmark
#endif // HAVE_BENCHMARK

@ -1206,7 +1206,7 @@ namespace JsonSerializationTests
if (this->m_features.m_enableInitializationTest) if (this->m_features.m_enableInitializationTest)
{ {
auto instance = this->m_description.CreateDefaultInstance(); auto instance = this->m_description.CreateDefaultInstance();
typename TypeParam::Type compare = typename TypeParam::Type{}; AZStd::remove_cvref_t<typename TypeParam::Type> compare;
if (!this->m_description.AreEqual(*instance, compare)) if (!this->m_description.AreEqual(*instance, compare))
{ {
auto serializer = this->m_description.CreateSerializer(); auto serializer = this->m_description.CreateSerializer();

@ -198,11 +198,13 @@ namespace AzFramework
AZ::SerializeContext* m_serializeContext{ nullptr }; AZ::SerializeContext* m_serializeContext{ nullptr };
//! The priority at which this call will be executed. //! The priority at which this call will be executed.
SpawnablePriority m_priority{ SpawnablePriority_Default }; SpawnablePriority m_priority{ SpawnablePriority_Default };
//! Entity references are resolved by referring to the last entity spawned from a template entity in the spawnable. If this //! Entity references are resolved by referring to the most recent entity spawned from a template entity in the spawnable.
//! is set to false entities from previous spawn calls are not taken into account. If set to true entity references may be //! If the entity referred to hasn't been spawned yet, the reference will be resolved to the first one that *will* be spawned.
//! resolved to a previously spawned entity. A lookup table has to be constructed when true, which may negatively impact //! If this flag is set to "true", the id mappings will persist across SpawnEntites calls, and the entity references will resolve
//! performance, especially if a large number of entities are present on a ticket. //! correctly across them.
bool m_referencePreviouslySpawnedEntities{ false }; //! When "false", the entity id mappings will be reset on this call, so entity references will only work within this call, or
//! potentially with any subsequent SpawnEntities call where the flag is true once again.
bool m_referencePreviouslySpawnedEntities{ true };
}; };
struct DespawnAllEntitiesOptionalArgs final struct DespawnAllEntitiesOptionalArgs final

@ -250,10 +250,47 @@ namespace AzFramework
AZ::Entity* SpawnableEntitiesManager::CloneSingleEntity(const AZ::Entity& entityTemplate, AZ::Entity* SpawnableEntitiesManager::CloneSingleEntity(const AZ::Entity& entityTemplate,
EntityIdMap& templateToCloneMap, AZ::SerializeContext& serializeContext) EntityIdMap& templateToCloneMap, AZ::SerializeContext& serializeContext)
{ {
return AZ::IdUtils::Remapper<AZ::EntityId, true>::CloneObjectAndGenerateNewIdsAndFixRefs( // If the same ID gets remapped more than once, preserve the original remapping instead of overwriting it.
constexpr bool allowDuplicateIds = false;
return AZ::IdUtils::Remapper<AZ::EntityId, allowDuplicateIds>::CloneObjectAndGenerateNewIdsAndFixRefs(
&entityTemplate, templateToCloneMap, &serializeContext); &entityTemplate, templateToCloneMap, &serializeContext);
} }
void SpawnableEntitiesManager::InitializeEntityIdMappings(
const Spawnable::EntityList& entities, EntityIdMap& idMap, AZStd::unordered_set<AZ::EntityId>& previouslySpawned)
{
// Make sure we don't have any previous data lingering around.
idMap.clear();
previouslySpawned.clear();
idMap.reserve(entities.size());
previouslySpawned.reserve(entities.size());
for (auto& entity : entities)
{
idMap.emplace(entity->GetId(), AZ::Entity::MakeId());
}
}
void SpawnableEntitiesManager::RefreshEntityIdMapping(
const AZ::EntityId& entityId, EntityIdMap& idMap, AZStd::unordered_set<AZ::EntityId>& previouslySpawned)
{
if (previouslySpawned.contains(entityId))
{
// This entity has already been spawned at least once before, so we need to generate a new id for it and
// preserve the new id to fix up any future entity references to this entity.
idMap[entityId] = AZ::Entity::MakeId();
}
else
{
// This entity hasn't been spawned yet, so use the first id we've already generated for this entity and mark
// it as spawned so we know not to reuse this id next time.
previouslySpawned.emplace(entityId);
}
}
bool SpawnableEntitiesManager::ProcessRequest(SpawnAllEntitiesCommand& request) bool SpawnableEntitiesManager::ProcessRequest(SpawnAllEntitiesCommand& request)
{ {
Ticket& ticket = *request.m_ticket; Ticket& ticket = *request.m_ticket;
@ -269,18 +306,24 @@ namespace AzFramework
const Spawnable::EntityList& entitiesToSpawn = ticket.m_spawnable->GetEntities(); const Spawnable::EntityList& entitiesToSpawn = ticket.m_spawnable->GetEntities();
size_t entitiesToSpawnSize = entitiesToSpawn.size(); 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 // Reserve buffers
spawnedEntities.reserve(spawnedEntities.size() + entitiesToSpawnSize); spawnedEntities.reserve(spawnedEntities.size() + entitiesToSpawnSize);
spawnedEntityIndices.reserve(spawnedEntityIndices.size() + entitiesToSpawnSize); spawnedEntityIndices.reserve(spawnedEntityIndices.size() + entitiesToSpawnSize);
templateToCloneEntityIdMap.reserve(entitiesToSpawnSize);
// Pre-generate the full set of entity id to new entity id mappings, so that during the clone operation below,
// any entity references that point to a not-yet-cloned entity will still get their ids remapped correctly.
// We clear out and regenerate the set of IDs on every SpawnAllEntities call, because presumably every entity reference
// in every entity we're about to instantiate is intended to point to an entity in our newly-instantiated batch, regardless
// of spawn order. If we didn't clear out the map, it would be possible for some entities here to have references to
// previously-spawned entities from a previous SpawnEntities or SpawnAllEntities call.
InitializeEntityIdMappings(entitiesToSpawn, ticket.m_entityIdReferenceMap, ticket.m_previouslySpawned);
for (size_t i = 0; i < entitiesToSpawnSize; ++i) for (size_t i = 0; i < entitiesToSpawnSize; ++i)
{ {
AZ::Entity* clone = CloneSingleEntity(*entitiesToSpawn[i], templateToCloneEntityIdMap, *request.m_serializeContext); // If this entity has previously been spawned, give it a new id in the reference map
RefreshEntityIdMapping(entitiesToSpawn[i].get()->GetId(), ticket.m_entityIdReferenceMap, ticket.m_previouslySpawned);
AZ::Entity* clone = CloneSingleEntity(*entitiesToSpawn[i], ticket.m_entityIdReferenceMap, *request.m_serializeContext);
AZ_Assert(clone != nullptr, "Failed to clone spawnable entity."); AZ_Assert(clone != nullptr, "Failed to clone spawnable entity.");
spawnedEntities.emplace_back(clone); spawnedEntities.emplace_back(clone);
@ -337,21 +380,17 @@ namespace AzFramework
const Spawnable::EntityList& entitiesToSpawn = ticket.m_spawnable->GetEntities(); const Spawnable::EntityList& entitiesToSpawn = ticket.m_spawnable->GetEntities();
size_t entitiesToSpawnSize = request.m_entityIndices.size(); size_t entitiesToSpawnSize = request.m_entityIndices.size();
// Reconstruct the template to entity mapping. if (ticket.m_entityIdReferenceMap.empty() || !request.m_referencePreviouslySpawnedEntities)
EntityIdMap templateToCloneEntityIdMap;
if (!request.m_referencePreviouslySpawnedEntities)
{
templateToCloneEntityIdMap.reserve(entitiesToSpawnSize);
}
else
{
templateToCloneEntityIdMap.reserve(spawnedEntitiesInitialCount + entitiesToSpawnSize);
SpawnableConstIndexEntityContainerView indexEntityView(
spawnedEntities.begin(), spawnedEntityIndices.begin(), spawnedEntities.size());
for (auto& entry : indexEntityView)
{ {
templateToCloneEntityIdMap.insert_or_assign(entitiesToSpawn[entry.GetIndex()]->GetId(), entry.GetEntity()->GetId()); // This map keeps track of ids from template (spawnable) to clone (instance) allowing patch ups of fields referring
} // to entityIds outside of a given entity.
// We pre-generate the full set of entity id to new entity id mappings, so that during the clone operation below,
// any entity references that point to a not-yet-cloned entity will still get their ids remapped correctly.
// By default, we only initialize this map once because it needs to persist across multiple SpawnEntities calls, so
// that reference fixups work even when the entity being referenced is spawned in a different SpawnEntities
// (or SpawnAllEntities) call.
// However, the caller can also choose to reset the map by passing in "m_referencePreviouslySpawnedEntities = false".
InitializeEntityIdMappings(entitiesToSpawn, ticket.m_entityIdReferenceMap, ticket.m_previouslySpawned);
} }
spawnedEntities.reserve(spawnedEntities.size() + entitiesToSpawnSize); spawnedEntities.reserve(spawnedEntities.size() + entitiesToSpawnSize);
@ -361,7 +400,12 @@ namespace AzFramework
{ {
if (index < entitiesToSpawn.size()) if (index < entitiesToSpawn.size())
{ {
AZ::Entity* clone = CloneSingleEntity(*entitiesToSpawn[index], templateToCloneEntityIdMap, *request.m_serializeContext); // If this entity has previously been spawned, give it a new id in the reference map
RefreshEntityIdMapping(
entitiesToSpawn[index].get()->GetId(), ticket.m_entityIdReferenceMap, ticket.m_previouslySpawned);
AZ::Entity* clone =
CloneSingleEntity(*entitiesToSpawn[index], ticket.m_entityIdReferenceMap, *request.m_serializeContext);
AZ_Assert(clone != nullptr, "Failed to clone spawnable entity."); AZ_Assert(clone != nullptr, "Failed to clone spawnable entity.");
spawnedEntities.push_back(clone); spawnedEntities.push_back(clone);
@ -451,9 +495,11 @@ namespace AzFramework
ticket.m_spawnedEntities.clear(); ticket.m_spawnedEntities.clear();
const Spawnable::EntityList& entities = request.m_spawnable->GetEntities(); const Spawnable::EntityList& entities = request.m_spawnable->GetEntities();
// Map keeps track of ids from template (spawnable) to clone (instance) // Pre-generate the full set of entity id to new entity id mappings, so that during the clone operation below,
// Allowing patch ups of fields referring to entityIds outside of a given entity // any entity references that point to a not-yet-cloned entity will still get their ids remapped correctly.
EntityIdMap templateToCloneEntityIdMap; // This map is intentionally cleared out and regenerated here to ensure that we're starting fresh with mappings that
// match the new set of template entities getting spawned.
InitializeEntityIdMappings(entities, ticket.m_entityIdReferenceMap, ticket.m_previouslySpawned);
if (ticket.m_loadAll) if (ticket.m_loadAll)
{ {
@ -461,11 +507,13 @@ namespace AzFramework
// to spawn every entity, simply start over. // to spawn every entity, simply start over.
ticket.m_spawnedEntityIndices.clear(); ticket.m_spawnedEntityIndices.clear();
size_t entitiesToSpawnSize = entities.size(); size_t entitiesToSpawnSize = entities.size();
templateToCloneEntityIdMap.reserve(entitiesToSpawnSize);
for (size_t i = 0; i < entitiesToSpawnSize; ++i) for (size_t i = 0; i < entitiesToSpawnSize; ++i)
{ {
AZ::Entity* clone = CloneSingleEntity(*entities[i], templateToCloneEntityIdMap, *request.m_serializeContext); // If this entity has previously been spawned, give it a new id in the reference map
RefreshEntityIdMapping(entities[i].get()->GetId(), ticket.m_entityIdReferenceMap, ticket.m_previouslySpawned);
AZ::Entity* clone = CloneSingleEntity(*entities[i], ticket.m_entityIdReferenceMap, *request.m_serializeContext);
AZ_Assert(clone != nullptr, "Failed to clone spawnable entity."); AZ_Assert(clone != nullptr, "Failed to clone spawnable entity.");
ticket.m_spawnedEntities.push_back(clone); ticket.m_spawnedEntities.push_back(clone);
@ -475,7 +523,7 @@ namespace AzFramework
else else
{ {
size_t entitiesSize = entities.size(); size_t entitiesSize = entities.size();
templateToCloneEntityIdMap.reserve(entitiesSize);
for (size_t index : ticket.m_spawnedEntityIndices) for (size_t index : ticket.m_spawnedEntityIndices)
{ {
// It's possible for the new spawnable to have a different number of entities, so guard against this. // It's possible for the new spawnable to have a different number of entities, so guard against this.
@ -483,7 +531,10 @@ namespace AzFramework
// detected and will result in the incorrect entities being spawned. // detected and will result in the incorrect entities being spawned.
if (index < entitiesSize) if (index < entitiesSize)
{ {
AZ::Entity* clone = CloneSingleEntity(*entities[index], templateToCloneEntityIdMap, *request.m_serializeContext); // If this entity has previously been spawned, give it a new id in the reference map
RefreshEntityIdMapping(entities[index].get()->GetId(), ticket.m_entityIdReferenceMap, ticket.m_previouslySpawned);
AZ::Entity* clone = CloneSingleEntity(*entities[index], ticket.m_entityIdReferenceMap, *request.m_serializeContext);
AZ_Assert(clone != nullptr, "Failed to clone spawnable entity."); AZ_Assert(clone != nullptr, "Failed to clone spawnable entity.");
ticket.m_spawnedEntities.push_back(clone); ticket.m_spawnedEntities.push_back(clone);
} }

@ -85,6 +85,22 @@ namespace AzFramework
AZ_CLASS_ALLOCATOR(Ticket, AZ::ThreadPoolAllocator, 0); AZ_CLASS_ALLOCATOR(Ticket, AZ::ThreadPoolAllocator, 0);
static constexpr uint32_t Processing = AZStd::numeric_limits<uint32_t>::max(); static constexpr uint32_t Processing = AZStd::numeric_limits<uint32_t>::max();
//! Map of template entity ids to their associated instance ids.
//! Tickets can be used to spawn the same template entities multiple times, in any order, across multiple calls.
//! Since template entities can reference other entities, this map is used to fix up those references across calls
//! using the following policy:
//! - Entities referencing an entity that hasn't been spawned yet will get a reference to the id that *will* be used
//! the first time that entity will be spawned. The reference will be invalid until that entity is spawned, but
//! will be valid if/when it gets spawned.
//! - Entities referencing an entity that *has* been spawned will get a reference to the id that was *last* used to
//! spawn the entity.
//! Note that this implies a certain level of non-determinism when spawning across calls, because the entity references
//! will be based on the order in which the SpawnEntity calls occur, which can be affected by things like priority.
EntityIdMap m_entityIdReferenceMap;
//! For this to work, we also need to keep track of whether or not each entity has been spawned at least once, so we know
//! whether or not to replace the id in the map when spawning a new instance of that entity.
AZStd::unordered_set<AZ::EntityId> m_previouslySpawned;
AZStd::vector<AZ::Entity*> m_spawnedEntities; AZStd::vector<AZ::Entity*> m_spawnedEntities;
AZStd::vector<size_t> m_spawnedEntityIndices; AZStd::vector<size_t> m_spawnedEntityIndices;
AZ::Data::Asset<Spawnable> m_spawnable; AZ::Data::Asset<Spawnable> m_spawnable;
@ -194,6 +210,15 @@ namespace AzFramework
bool ProcessRequest(BarrierCommand& request); bool ProcessRequest(BarrierCommand& request);
bool ProcessRequest(DestroyTicketCommand& request); bool ProcessRequest(DestroyTicketCommand& request);
//! Generate a base set of original-to-new entity ID mappings to use during spawning.
//! Since Entity references get fixed up on an entity-by-entity basis while spawning, it's important to have the complete
//! set of new IDs available right at the start. This way, entities that refer to other entities that haven't spawned yet
//! will still get their references remapped correctly.
void InitializeEntityIdMappings(
const Spawnable::EntityList& entities, EntityIdMap& idMap, AZStd::unordered_set<AZ::EntityId>& previouslySpawned);
void RefreshEntityIdMapping(
const AZ::EntityId& entityId, EntityIdMap& idMap, AZStd::unordered_set<AZ::EntityId>& previouslySpawned);
Queue m_highPriorityQueue; Queue m_highPriorityQueue;
Queue m_regularPriorityQueue; Queue m_regularPriorityQueue;

@ -75,6 +75,8 @@ namespace AzFramework
// Add a fullscreen button in the upper right of the title bar. // Add a fullscreen button in the upper right of the title bar.
[m_nativeWindow setCollectionBehavior:NSWindowCollectionBehaviorFullScreenPrimary]; [m_nativeWindow setCollectionBehavior:NSWindowCollectionBehaviorFullScreenPrimary];
m_nativeWindow.tabbingMode = NSWindowTabbingModeDisallowed;
// Make the window active // Make the window active
[m_nativeWindow makeKeyAndOrderFront:nil]; [m_nativeWindow makeKeyAndOrderFront:nil];
m_nativeWindow.title = m_windowTitle; m_nativeWindow.title = m_windowTitle;

@ -44,6 +44,11 @@ namespace AzNetworking
//! @class IConnection //! @class IConnection
//! @brief interface class for network connections. //! @brief interface class for network connections.
//!
//! IConnection provides a pure-virtual interface for all network connection types. IConnections provide access to
//! a ConnectionMetrics object which provides a variety of metrics on the connection itself such as data rate, RTT and
//! packet statistics.
class IConnection class IConnection
{ {
public: public:

@ -22,6 +22,12 @@ namespace AzNetworking
{ {
//! @class IConnectionListener //! @class IConnectionListener
//! @brief interface class for application layer dealing with connection level events. //! @brief interface class for application layer dealing with connection level events.
//!
//! IConnectionListener defines an abstract interface that the user of AzNetworking is expected to implement to react and
//! handle all IConnection related events, including the handling of any received IPacket derived packets. The AzNetworking
//! user should derive a handler class from IConnectionListener, and provide an instance of that handler to any
//! INetworkInterface the user instantiates. The lifetime of the IConnectionListener must outlive the lifetime of the
//! INetworkInterface.
class IConnectionListener class IConnectionListener
{ {
public: public:

@ -18,6 +18,11 @@ namespace AzNetworking
{ {
//! @class IConnectionSet //! @class IConnectionSet
//! @brief interface class for managing a set of connections. //! @brief interface class for managing a set of connections.
//!
//! IConnectionSet defines a simple interface for working with an abstract set of IConnections bound to an
//! INetworkInterface. Generally users of AzNetworking will not have reason to interact directly with the IConnectionSet,
//! as its interface is completely wrapped by INetworkInterface.
class IConnectionSet class IConnectionSet
{ {
public: public:

@ -23,10 +23,10 @@ namespace AzNetworking
//! Collection of compression related error codes //! Collection of compression related error codes
enum class CompressorError enum class CompressorError
{ {
Ok, ///< No error, operation finished successfully Ok, //!< No error, operation finished successfully
InsufficientBuffer, ///< Buffer size is insufficient for the operation to complete, increase the size and try again InsufficientBuffer, //!< Buffer size is insufficient for the operation to complete, increase the size and try again
CorruptData, ///< Malformed or hacked packet, potentially security issue CorruptData, //!< Malformed or hacked packet, potentially security issue
Uninitialized ///< Compressor or supplied buffers are uninitialized Uninitialized //!< Compressor or supplied buffers are uninitialized
}; };
//! Unique identifier of a given compressor //! Unique identifier of a given compressor
@ -34,6 +34,12 @@ namespace AzNetworking
//! @class ICompressor //! @class ICompressor
//! @brief Packet data compressor interface. //! @brief Packet data compressor interface.
//!
//! ICompressor is an abstract compression interface meant for user provided GEMs to implement (such as the [Multiplayer
//! Compression Gem](http://docs.o3de.org/docs/user-guide/gems/reference/multiplayer-compression)).
//! Compression is supported for both TCP and UDP connections. Instantiation of a compressor is controlled by the
//! `net_UdpCompressor` or `net_TcpCompressor` cvar for their respective protocols.
class ICompressor class ICompressor
{ {
public: public:
@ -87,8 +93,16 @@ namespace AzNetworking
) = 0; ) = 0;
}; };
//! Abstract factory to instantiate compressors. //! @class ICompressorFactory
//! Used by the network interface to create a compressor //! @brief Abstract factory to instantiate compressors.
//!
//! ICompressorFactory is an abstract compression interface meant for user provided GEMs to implement. ICompressorFactory
//! implementations can be registered to classes implementing INetworking. Registered factories can then be used to create
//! ICompressor implementations on demand. The [Multiplayer Compression
//! Gem](http://docs.o3de.org/docs/user-guide/gems/reference/multiplayer-compression) is an example of an ICompressorFactory
//! for an LZ4 Compressor. In it, MultiplayerCompressionSystemComponent registers its ICompressorFactory with
//! NetworkingSystemComponent, which is an implementation of INetworking. Registered factories are keyed by their AZ Name
//! which is accessed through the factory's GetFactoryName method.
class ICompressorFactory class ICompressorFactory
{ {
public: public:

@ -22,7 +22,16 @@
namespace AzNetworking namespace AzNetworking
{ {
//! @class INetworkInterface //! @class INetworkInterface
//! @brief pure virtual network interface class to abstract client/server and tcp/udp concerns from application code. //! @brief Network interface class to abstract client/server and protocol concerns from application code.
//!
//! INetworkInterface provides an abstract API capable of receiving and opening IConnection objects, sending IPacket objects with optional
//! reliability, and determining the delivery status of packets that have been sent unreliably (delivery of reliable packets
//! is guaranteed as long as the associated connection remains open). INetworkInterface must be provided an
//! IConnectionListener instance that outlives the INetworkInterface itself. The INetworkInterface also creates and manages
//! the IConnectionSet, which tracks all open connections bound to the interface. INetworkInterface also provides GetMetrics
//! functions which can be used to fetch a struct detailing a variety of metrics relating to send and receive rates for both
//! packets and bytes in addition to the effect of features on those rates (such as packet size reduction due to compression.)
class INetworkInterface class INetworkInterface
{ {
public: public:

@ -23,6 +23,17 @@ namespace AzNetworking
//! @class INetworking //! @class INetworking
//! @brief The interface for creating and working with network interfaces. //! @brief The interface for creating and working with network interfaces.
//!
//! INetworking is an Az::Interface<T> that provides applications access to higher level networking abstractions.
//! AzNetworking::INetworking can be used to instantiate new INetworkInterfaces that can be configured to operate over
//! either TCP or UDP, enable or disable encryption, and be assigned a trust level.
//!
//! INetworking is also responsible for registering ICompressorFactory implementations. This allows a developer to have
//! access to multiple ICompressorFactory implementations by name. The [MultiplayerCompressor
//! Gem](http://docs.o3de.org/docs/user-guide/gems/reference/multiplayer-compression) is an example of this using the
//! [LZ4](https://wikipedia.org/wiki/LZ4_%28compression_algorithm%29) algorithm.
//!
class INetworking class INetworking
{ {
public: public:

@ -24,6 +24,15 @@ namespace AzNetworking
//! @class IPacket //! @class IPacket
//! @brief Base class for all packets. //! @brief Base class for all packets.
//!
//! IPacket defines an abstract interface that all packets transmitted using AzNetworking must conform to. While there are
//! a number of core packets used internally by AzNetworking, it is fully possible for end-users to define their own custom
//! packets using this interface. PacketType should be distinct, and should be greater than
//! AzNetworking::CorePackets::MAX. The Serialize method allows the IPacket to be used by an
//! ISerializer to move data between hosts safely and efficiently.
//!
//! For more information on the packet format and best practices for extending the packet system, read
//! [Networking Packets](http://docs.o3de.org/docs/user-guide/networking/packets) on the O3DE documentation site.
class IPacket class IPacket
{ {
public: public:

@ -28,6 +28,19 @@ namespace AzNetworking
//! @class IPacketHeader //! @class IPacketHeader
//! @brief A packet header that lets us deduce packet type for any incoming packet. //! @brief A packet header that lets us deduce packet type for any incoming packet.
//!
//! IPacketHeader defines an abstract interface for a descriptor of all AzNetworking::IPacket sent through AzNetworking. The
//! PacketHeader is used to identify and describe the contents of a Packet so that transport logic can identify what
//! additional processing steps need to be taken (if any) and what type of Packet is being inspected.
//!
//! The PacketFlags portion of the header represents the first byte of the header. While it can be encrypted it is
//! otherwise not exposed to additional processing (such as an AzNetworking::ICompressor). PacketFlags are a bitfield use to provide up
//! front information about the state of the packet. Currently there is only one flag to indicate if the Packet is
//! compressed or not.
//!
//! The remainder of the header contains the PacketType and the PacketId. While the PacketFlags byte is exempt from most
//! additional forms of processing, the remainder of the header is not.
class IPacketHeader class IPacketHeader
{ {
public: public:

@ -27,6 +27,18 @@ namespace AzNetworking
//! @class ISerializer //! @class ISerializer
//! @brief Interface class for all serializers to derive from. //! @brief Interface class for all serializers to derive from.
//!
//! ISerializer defines an abstract interface for visiting an object hierarchy and performing operations upon that hierarchy,
//! typically reading from or writing data to the object hierarchy for reasons of persistence or network transmission.
//!
//! While the most common types of serializers are provided by the AzNetworking framework, users can implement custom
//! serializers and perform complex operations on any serializable structures. A few types native to AzNetworking, many of which
//! relate to packets, demonstrate this.
//!
//! Provided serializers include NetworkInputSerializer for writing an object model into a bytestream, NetworkOutputSerializer
//! for writing to an object model, TrackChangesSerializer which is used to efficiently serialize objects without incurring significant
//! copy or comparison overhead, and HashSerializer which can be used to generate a hash of all visited data which is important for
//! automated desync detection.
class ISerializer class ISerializer
{ {
public: public:

@ -25,6 +25,44 @@ namespace AzNetworking
//! @class TcpNetworkInterface //! @class TcpNetworkInterface
//! @brief This class implements a TCP network interface. //! @brief This class implements a TCP network interface.
//!
//! TcpNetworkInterface is an implementation of AzNetworking::INetworkInterface.
//! Unlike UDP, TCP implements a variety of transport features such as congestion
//! avoidance, flow control, and reliability. These features are valuable, but TCP
//! offers minimal configuration of them. This is why UdpNetworkInterface offers
//! similar features, but with greater flexibility in configuration. If your project doesn't
//! require the low latency of UDP, consider using TCP.
//!
//! ## Packet structure
//!
//! * Flags - A bitfield a receiving endpoint can quickly inspect to learn about configuration of a packet
//! * Header - Details the type of packet and other information related to reliability
//! * Payload - The actual serialized content of the packet
//!
//! For more information, read [Networking Packets](http://docs.o3de.org/docs/user-guide/networking/packets) in the O3DE documentation.
//!
//! ## Reliability
//!
//! TCP packets can only be sent reliably. This is a feature of TCP itself.
//!
//! ## Fragmentation
//!
//! TCP implements fragmentation under the hood. Consumers of TCP packets will never
//! need to worry about reconstructing the contents over multiple transmissions.
//!
//! ## Compression
//!
//! Compression here refers to content insensitive compression using libraries like
//! LZ4. If enabled, the target payload is run through the compressor and replaces
//! the original payload if it's in fact smaller. To tell if compression is enabled
//! on a given packet, we operate on a bit in the packet's Flags. The Sender writes
//! this bit while the Receiver checks it to see if a packet needs to be
//! decompressed.
//!
//! ## Encryption
//!
//! AzNetworking uses the [OpenSSL](https://www.openssl.org/) library to implement TLS encryption. If enabled,
//! the O3DE network layer handles the OpenSSL handshake under the hood using provided certificates.
class TcpNetworkInterface final class TcpNetworkInterface final
: public INetworkInterface : public INetworkInterface
{ {

@ -27,12 +27,58 @@ namespace AzNetworking
class IConnectionListener; class IConnectionListener;
class ICompressor; class ICompressor;
// 20 byte IPv4 header + 8 byte UDP header static const uint32_t UdpPacketHeaderSize = 20 + 8; //!< 20 byte IPv4 header + 8 byte UDP header
static const uint32_t UdpPacketHeaderSize = 20 + 8; static const uint32_t DtlsPacketHeaderSize = 13; //!< DTLS1_RT_HEADER_LENGTH
static const uint32_t DtlsPacketHeaderSize = 13; // DTLS1_RT_HEADER_LENGTH
//! @class UdpNetworkInterface //! @class UdpNetworkInterface
//! @brief This class implements a UDP network interface. //! @brief This class implements a UDP network interface.
//!
//! UdpNetworkInterface is an implementation of AzNetworking::INetworkInterface. Since UDP is a very bare bones protocol,
//! the Open 3D Engine implementation has to provide significantly more than its TCP counterpart (since TCP implements a
//! significant number of reliability features.)
//!
//! When sent through UDP, a packet can have additional actions performed on it depending on which features are enabled and
//! configured. Each feature listed in this description is in the order a packet will see them on Send.
//!
//! ### Packet structure
//!
//! The general structure of a UDP packet is:
//!
//! * Flags - A bitfield a receiving endpoint can quickly inspect to learn about configuration of a packet
//! * Header - Details the type of packet and other information related to reliability
//! * Payload - The actual serialized content of the packet
//!
//! For more information, read [Networking Packets](http://docs.o3de.org/docs/user-guide/networking/packets) in the O3DE documentation.
//!
//! ### Reliability
//!
//! UDP packets can be sent reliably or unreliably. Reliably sent packets are registered for tracking first. This causes the
//! reliable packet to be resent if a timeout on the packet is reached. Once the packet is acknowledged, the packet is
//! unregistered.
//!
//! ### Fragmentation
//!
//! If the raw packet size exceeds the configured maximum transmission unit (MTU) then the packet is broken into
//! multiple reliable fragments to avoid fragmentation at the routing level. Fragments are always reliable so the original
//! packet can be reconstructed. Operations that alter the payload generally follow this step so that they can be
//! separately applied to the Fragments in addition to not being applied to both the original and Fragments.
//!
//! ### Compression
//!
//! Compression here refers to content insensitive compression using libraries like LZ4. If enabled, the target payload is
//! run through the compressor and replaces the original payload if it's in fact smaller. To tell if compression is enabled
//! on a given packet, we operate on a bit in the packet's Flags. The Sender writes this bit while the Receiver checks it to
//! see if a packet needs to be decompressed.
//!
//! O3DE could potentially move from over MTU to under with compression, and the UDP interface doesn't check for this. Detecting a change
//! that would reduce the number of fragmented packets would require pre-emptively compressing payloads to tell if that change happened,
//! which could potentially lead to a lot of unnecessary calls to the compressor.
//!
//! ### Encryption
//!
//! AzNetworking uses the [OpenSSL](https://www.openssl.org/) library to implement Datagram Layer Transport Security (DTLS) encryption
//! on UDP traffic. Encryption operates as described in [O3DE Networking Encryption](http://docs.o3de.org/docs/user-guide/networking/encryption)
//! on the documentation website. Once both endpoints have completed their handshake, all traffic is expected to be fully encrypted.
class UdpNetworkInterface final class UdpNetworkInterface final
: public INetworkInterface : public INetworkInterface
{ {

@ -493,6 +493,29 @@ namespace AzQtComponents
} }
} }
break; break;
case CE_MenuItem:
{
const QMenu* menu = qobject_cast<const QMenu*>(widget);
QAction* action = menu->activeAction();
if (action)
{
QMenu* subMenu = action->menu();
if (subMenu)
{
QVariant noHover = subMenu->property("noHover");
if (noHover.isValid() && noHover.toBool())
{
// First draw as standard to get the correct hover background for the complete control.
QProxyStyle::drawControl(element, option, painter, widget);
// Now draw the icon as non-hovered so control behaves as designed.
QStyleOptionMenuItem myOpt = *qstyleoption_cast<const QStyleOptionMenuItem*>(option);
myOpt.state &= ~QStyle::State_Selected;
return QProxyStyle::drawControl(element, &myOpt, painter, widget);
}
}
}
}
break;
} }
return QProxyStyle::drawControl(element, option, painter, widget); return QProxyStyle::drawControl(element, option, painter, widget);

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Helpers Icon</title>
<defs>
<circle id="path-1" cx="8" cy="8" r="8"></circle>
<path d="M8.59736328,10.1821289 L8.59736328,9.79394531 C8.59736328,9.07617188 8.86835937,8.71728516 9.84248047,8.15332031 C10.8751953,7.54541016 11.4098633,6.77636719 11.4098633,5.67041016 C11.4098633,4.00048828 10.0255859,2.8359375 7.93085937,2.8359375 C5.68232422,2.8359375 4.40791016,4.09570312 4.37128906,5.89746094 L6.35615234,5.89746094 C6.40009766,5.06982422 6.95673828,4.53515625 7.79902344,4.53515625 C8.63398437,4.53515625 9.190625,5.04052734 9.190625,5.73632812 C9.190625,6.43212891 8.90498047,6.79101562 7.96015625,7.35498047 C6.94941406,7.94824219 6.54658203,8.60742188 6.64179687,9.75732422 L6.65644531,10.1821289 L8.59736328,10.1821289 Z M7.70380859,13.7124023 C8.52412109,13.7124023 9.02949219,13.2436523 9.02949219,12.4892578 C9.02949219,11.7275391 8.52412109,11.2587891 7.70380859,11.2587891 C6.88349609,11.2587891 6.37080078,11.7275391 6.37080078,12.4892578 C6.37080078,13.2436523 6.88349609,13.7124023 7.70380859,13.7124023 Z" id="path-3"></path>
</defs>
<g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="2nd-ToolBar-v2-(Perspective)-3" transform="translate(-1161.000000, -7.000000)">
<g id="2nd-ToolBar-buttons-on-right" transform="translate(1076.000000, 5.000000)">
<g id="Helpers-Icon" transform="translate(85.000000, 2.000000)">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<use id="Oval" fill="#FFFFFF" xlink:href="#path-1"></use>
<g id="?" fill-rule="nonzero" mask="url(#mask-2)">
<use fill="#909090" xlink:href="#path-3"></use>
<use fill="#3F3F3F" xlink:href="#path-3"></use>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Buttons / Dropdown button with Icon / no arrow</title>
<g id="Buttons-/-Dropdown-button-with-Icon-/-no-arrow" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Icons-/-System-/-Menu" fill="#FFFFFF">
<rect id="Rectangle-11" x="1.33333333" y="2.66666667" width="13.3333333" height="1.33333333"></rect>
<rect id="Rectangle-11" x="1.33333333" y="7.33333333" width="13.3333333" height="1.33333333"></rect>
<rect id="Rectangle-11" x="1.33333333" y="12" width="13.3333333" height="1.33333333"></rect>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 786 B

@ -1,4 +1,4 @@
<svg width="13" height="13" viewBox="0 0 13 13" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="13" height="13" viewBox="0 0 13 13" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.0708 0.87087L11.0708 0.87074C10.4286 0.371024 9.61107 0.13517 8.79265 0.213464C7.97422 0.291757 7.21961 0.678004 6.6897 1.28985L5.76756 2.36366C5.63378 2.51944 5.56909 2.72055 5.58771 2.92273C5.60634 3.12492 5.70675 3.31163 5.86686 3.44178C6.02698 3.57194 6.23368 3.63488 6.44149 3.61676C6.6493 3.59864 6.8412 3.50094 6.97498 3.34516L7.89716 2.27122C8.03184 2.11488 8.19736 1.98639 8.38395 1.89334C8.57053 1.8003 8.7744 1.74459 8.98349 1.7295C9.19258 1.71442 9.40266 1.74027 9.60131 1.80552C9.79996 1.87078 9.98315 1.97411 10.1401 2.10942C10.4422 2.38256 10.6244 2.75851 10.6488 3.15904C10.6732 3.55957 10.5379 3.95383 10.2711 4.25978L8.35381 6.49309L8.33616 6.51209C8.23375 6.62767 8.11374 6.72729 7.98031 6.80749C7.67189 6.99364 7.30661 7.06983 6.94686 7.02305C6.58711 6.97626 6.25522 6.8094 6.00789 6.55097C5.86556 6.40288 5.66865 6.31578 5.46039 6.30879C5.25213 6.30179 5.04952 6.37546 4.89703 6.51365C4.74453 6.65183 4.65462 6.84323 4.64701 7.04584C4.6394 7.24845 4.71472 7.44572 4.85645 7.59436C5.21222 7.96578 5.65746 8.24515 6.15198 8.40726C6.6465 8.56937 7.17473 8.60911 7.68898 8.52289C7.84983 8.49539 8.00828 8.45599 8.16298 8.40505C8.70939 8.22568 9.19408 7.90261 9.56335 7.47165L11.4758 5.24457C11.7478 4.92529 11.9523 4.55683 12.0773 4.16038C12.2024 3.76394 12.2457 3.34735 12.2046 2.93457C12.1671 2.53429 12.0475 2.1454 11.8527 1.79092C11.658 1.43644 11.3921 1.12358 11.0708 0.87087Z" fill="white"/> <path d="M11.0708 0.87087L11.0708 0.87074C10.4286 0.371024 9.61107 0.13517 8.79265 0.213464C7.97422 0.291757 7.21961 0.678004 6.6897 1.28985L5.76756 2.36366C5.63378 2.51944 5.56909 2.72055 5.58771 2.92273C5.60634 3.12492 5.70675 3.31163 5.86686 3.44178C6.02698 3.57194 6.23368 3.63488 6.44149 3.61676C6.6493 3.59864 6.8412 3.50094 6.97498 3.34516L7.89716 2.27122C8.03184 2.11488 8.19736 1.98639 8.38395 1.89334C8.57053 1.8003 8.7744 1.74459 8.98349 1.7295C9.19258 1.71442 9.40266 1.74027 9.60131 1.80552C9.79996 1.87078 9.98315 1.97411 10.1401 2.10942C10.4422 2.38256 10.6244 2.75851 10.6488 3.15904C10.6732 3.55957 10.5379 3.95383 10.2711 4.25978L8.35381 6.49309L8.33616 6.51209C8.23375 6.62767 8.11374 6.72729 7.98031 6.80749C7.67189 6.99364 7.30661 7.06983 6.94686 7.02305C6.58711 6.97626 6.25522 6.8094 6.00789 6.55097C5.86556 6.40288 5.66865 6.31578 5.46039 6.30879C5.25213 6.30179 5.04952 6.37546 4.89703 6.51365C4.74453 6.65183 4.65462 6.84323 4.64701 7.04584C4.6394 7.24845 4.71472 7.44572 4.85645 7.59436C5.21222 7.96578 5.65746 8.24515 6.15198 8.40726C6.6465 8.56937 7.17473 8.60911 7.68898 8.52289C7.84983 8.49539 8.00828 8.45599 8.16298 8.40505C8.70939 8.22568 9.19408 7.90261 9.56335 7.47165L11.4758 5.24457C11.7478 4.92529 11.9523 4.55683 12.0773 4.16038C12.2024 3.76394 12.2457 3.34735 12.2046 2.93457C12.1671 2.53429 12.0475 2.1454 11.8527 1.79092C11.658 1.43644 11.3921 1.12358 11.0708 0.87087Z" fill="#3F3F3F"/>
<path d="M5.40958 9.14055L4.58546 10.1003C4.32342 10.4101 3.94867 10.6097 3.53919 10.6576C3.12972 10.7054 2.71706 10.5977 2.3871 10.357C2.22234 10.2309 2.08525 10.0738 1.98392 9.89526C1.8826 9.71668 1.81911 9.52014 1.79719 9.31727C1.77528 9.1144 1.79539 8.90932 1.85634 8.71414C1.91729 8.51896 2.01784 8.33765 2.15204 8.18093L4.10231 5.90975L4.11666 5.89415C4.21908 5.77861 4.3391 5.67903 4.47254 5.59887C4.75216 5.42921 5.07976 5.34988 5.4085 5.37222C5.73725 5.39457 6.05033 5.51745 6.30299 5.72329C6.3635 5.77246 6.42015 5.82595 6.47246 5.88333C6.54738 5.9658 6.63971 6.03156 6.74316 6.07611C6.84661 6.12066 6.95872 6.14295 7.07184 6.14146C7.18361 6.13992 7.29372 6.11492 7.39465 6.06817C7.49558 6.02143 7.58495 5.95403 7.65666 5.8706L7.66607 5.85958C7.78641 5.72083 7.85132 5.54454 7.8489 5.36301C7.84647 5.18148 7.77687 5.00689 7.65286 4.87124C7.35341 4.54132 6.98422 4.27825 6.57057 4.10003C6.15692 3.92182 5.70858 3.83266 5.25623 3.83866C4.80388 3.84467 4.35821 3.94569 3.94971 4.13482C3.54122 4.32395 3.17954 4.59672 2.88945 4.93446L0.944626 7.19943C0.420065 7.81564 0.163643 8.60683 0.230025 9.40433C0.296406 10.2018 0.68034 10.9426 1.29998 11.4686C1.61272 11.7312 1.97644 11.9301 2.3696 12.0535C2.76277 12.1769 3.17738 12.2223 3.5889 12.187C3.687 12.1793 3.78434 12.1672 3.88093 12.1507C4.62877 12.0232 5.30663 11.6437 5.79562 11.0786L6.617 10.1221C6.75077 9.96628 6.81547 9.76517 6.79684 9.56299C6.77822 9.3608 6.6778 9.17409 6.51769 9.04394C6.35758 8.91378 6.15088 8.85084 5.94306 8.86896C5.73525 8.88708 5.54335 8.98477 5.40957 9.14055H5.40958Z" fill="white"/> <path d="M5.40958 9.14055L4.58546 10.1003C4.32342 10.4101 3.94867 10.6097 3.53919 10.6576C3.12972 10.7054 2.71706 10.5977 2.3871 10.357C2.22234 10.2309 2.08525 10.0738 1.98392 9.89526C1.8826 9.71668 1.81911 9.52014 1.79719 9.31727C1.77528 9.1144 1.79539 8.90932 1.85634 8.71414C1.91729 8.51896 2.01784 8.33765 2.15204 8.18093L4.10231 5.90975L4.11666 5.89415C4.21908 5.77861 4.3391 5.67903 4.47254 5.59887C4.75216 5.42921 5.07976 5.34988 5.4085 5.37222C5.73725 5.39457 6.05033 5.51745 6.30299 5.72329C6.3635 5.77246 6.42015 5.82595 6.47246 5.88333C6.54738 5.9658 6.63971 6.03156 6.74316 6.07611C6.84661 6.12066 6.95872 6.14295 7.07184 6.14146C7.18361 6.13992 7.29372 6.11492 7.39465 6.06817C7.49558 6.02143 7.58495 5.95403 7.65666 5.8706L7.66607 5.85958C7.78641 5.72083 7.85132 5.54454 7.8489 5.36301C7.84647 5.18148 7.77687 5.00689 7.65286 4.87124C7.35341 4.54132 6.98422 4.27825 6.57057 4.10003C6.15692 3.92182 5.70858 3.83266 5.25623 3.83866C4.80388 3.84467 4.35821 3.94569 3.94971 4.13482C3.54122 4.32395 3.17954 4.59672 2.88945 4.93446L0.944626 7.19943C0.420065 7.81564 0.163643 8.60683 0.230025 9.40433C0.296406 10.2018 0.68034 10.9426 1.29998 11.4686C1.61272 11.7312 1.97644 11.9301 2.3696 12.0535C2.76277 12.1769 3.17738 12.2223 3.5889 12.187C3.687 12.1793 3.78434 12.1672 3.88093 12.1507C4.62877 12.0232 5.30663 11.6437 5.79562 11.0786L6.617 10.1221C6.75077 9.96628 6.81547 9.76517 6.79684 9.56299C6.77822 9.3608 6.6778 9.17409 6.51769 9.04394C6.35758 8.91378 6.15088 8.85084 5.94306 8.86896C5.73525 8.88708 5.54335 8.98477 5.40957 9.14055H5.40958Z" fill="#3F3F3F"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

@ -28,5 +28,7 @@
<file alias="script_canvas_editor.svg">Menu/script_canvas_editor.svg</file> <file alias="script_canvas_editor.svg">Menu/script_canvas_editor.svg</file>
<file alias="trackview_editor.svg">Menu/trackview_editor.svg</file> <file alias="trackview_editor.svg">Menu/trackview_editor.svg</file>
<file alias="ui_editor.svg">Menu/ui_editor.svg</file> <file alias="ui_editor.svg">Menu/ui_editor.svg</file>
<file alias="menu.svg">Menu/menu.svg</file>
<file alias="helpers.svg">Menu/helpers.svg</file>
</qresource> </qresource>
</RCC> </RCC>

@ -524,8 +524,6 @@ namespace AzToolsFramework
rootSpawnableIndex = m_playInEditorData.m_assets.size(); rootSpawnableIndex = m_playInEditorData.m_assets.size();
} }
LoadReferencedAssets(product.GetReferencedAssets());
AZ::Data::AssetInfo info; AZ::Data::AssetInfo info;
info.m_assetId = product.GetAsset().GetId(); info.m_assetId = product.GetAsset().GetId();
info.m_assetType = product.GetAssetType(); info.m_assetType = product.GetAssetType();
@ -534,6 +532,19 @@ namespace AzToolsFramework
AZ::Data::AssetCatalogRequestBus::Broadcast( AZ::Data::AssetCatalogRequestBus::Broadcast(
&AZ::Data::AssetCatalogRequestBus::Events::RegisterAsset, info.m_assetId, info); &AZ::Data::AssetCatalogRequestBus::Events::RegisterAsset, info.m_assetId, info);
m_playInEditorData.m_assets.emplace_back(product.ReleaseAsset().release(), AZ::Data::AssetLoadBehavior::Default); m_playInEditorData.m_assets.emplace_back(product.ReleaseAsset().release(), AZ::Data::AssetLoadBehavior::Default);
// Ensure the product asset is registered with the AssetManager
// Hold on to the returned asset to keep ref count alive until we assign it the latest data
AZ::Data::Asset<AZ::Data::AssetData> asset =
AZ::Data::AssetManager::Instance().FindOrCreateAsset(info.m_assetId, info.m_assetType, AZ::Data::AssetLoadBehavior::Default);
// Update the asset registered in the AssetManager with the data of our product from the Prefab Processor
AZ::Data::AssetManager::Instance().AssignAssetData(m_playInEditorData.m_assets.back());
}
for (auto& product : context.GetProcessedObjects())
{
LoadReferencedAssets(product.GetReferencedAssets());
} }
// make sure that PRE_NOTIFY assets get their notify before we activate, so that we can preserve the order of // make sure that PRE_NOTIFY assets get their notify before we activate, so that we can preserve the order of

@ -326,6 +326,16 @@ namespace AzToolsFramework
return *(m_nestedInstances[newInstanceAlias] = std::move(instance)); return *(m_nestedInstances[newInstanceAlias] = std::move(instance));
} }
void Instance::DetachNestedInstances(const AZStd::function<void(AZStd::unique_ptr<Instance>)>& callback)
{
for (auto&& [instanceAlias, instance] : m_nestedInstances)
{
instance->m_parent = nullptr;
callback(AZStd::move(instance));
}
m_nestedInstances.clear();
}
AZStd::unique_ptr<Instance> Instance::DetachNestedInstance(const InstanceAlias& instanceAlias) AZStd::unique_ptr<Instance> Instance::DetachNestedInstance(const InstanceAlias& instanceAlias)
{ {
AZStd::unique_ptr<Instance> removedNestedInstance; AZStd::unique_ptr<Instance> removedNestedInstance;

@ -103,6 +103,7 @@ namespace AzToolsFramework
Instance& AddInstance(AZStd::unique_ptr<Instance> instance); Instance& AddInstance(AZStd::unique_ptr<Instance> instance);
Instance& AddInstance(AZStd::unique_ptr<Instance> instance, InstanceAlias instanceAlias); Instance& AddInstance(AZStd::unique_ptr<Instance> instance, InstanceAlias instanceAlias);
AZStd::unique_ptr<Instance> DetachNestedInstance(const InstanceAlias& instanceAlias); AZStd::unique_ptr<Instance> DetachNestedInstance(const InstanceAlias& instanceAlias);
void DetachNestedInstances(const AZStd::function<void(AZStd::unique_ptr<Instance>)>& callback);
/** /**
* Gets the aliases for the entities in the Instance DOM. * Gets the aliases for the entities in the Instance DOM.

@ -922,8 +922,8 @@ namespace AzToolsFramework
return AZ::Failure(AZStd::string("Failed to duplicate : Couldn't get a valid owning instance for the common root entity of the entities provided.")); return AZ::Failure(AZStd::string("Failed to duplicate : Couldn't get a valid owning instance for the common root entity of the entities provided."));
} }
// If the first entity id is a container entity id, then we need to mark its parent as the common owning instance because you // If the first entity id is a container entity id, then we need to mark its parent as the common owning instance
// cannot duplicate an instance from itself. // This is because containers, despite representing the nested instance in the parent, are owned by the child.
if (commonOwningInstance->get().GetContainerEntityId() == firstEntityIdToDuplicate) if (commonOwningInstance->get().GetContainerEntityId() == firstEntityIdToDuplicate)
{ {
commonOwningInstance = commonOwningInstance->get().GetParentInstance(); commonOwningInstance = commonOwningInstance->get().GetParentInstance();
@ -967,17 +967,18 @@ namespace AzToolsFramework
// Duplicate any nested entities and instances as requested // Duplicate any nested entities and instances as requested
AZStd::unordered_map<InstanceAlias, Instance*> newInstanceAliasToOldInstanceMap; AZStd::unordered_map<InstanceAlias, Instance*> newInstanceAliasToOldInstanceMap;
AZStd::unordered_map<EntityAlias, EntityAlias> duplicateEntityAliasMap;
DuplicateNestedEntitiesInInstance(commonOwningInstance->get(), DuplicateNestedEntitiesInInstance(commonOwningInstance->get(),
entities, instanceDomAfter, duplicatedEntityAndInstanceIds); entities, instanceDomAfter, duplicatedEntityAndInstanceIds, duplicateEntityAliasMap);
DuplicateNestedInstancesInInstance(commonOwningInstance->get(),
instances, instanceDomAfter, duplicatedEntityAndInstanceIds,
newInstanceAliasToOldInstanceMap);
PrefabUndoInstance* command = aznew PrefabUndoInstance("Entity/Instance duplication"); PrefabUndoInstance* command = aznew PrefabUndoInstance("Entity/Instance duplication");
command->SetParent(undoBatch.GetUndoBatch()); command->SetParent(undoBatch.GetUndoBatch());
command->Capture(instanceDomBefore, instanceDomAfter, commonOwningInstance->get().GetTemplateId()); command->Capture(instanceDomBefore, instanceDomAfter, commonOwningInstance->get().GetTemplateId());
command->Redo(); command->Redo();
DuplicateNestedInstancesInInstance(commonOwningInstance->get(),
instances, instanceDomAfter, duplicatedEntityAndInstanceIds, newInstanceAliasToOldInstanceMap);
// Create links for our duplicated instances (if any were duplicated) // Create links for our duplicated instances (if any were duplicated)
for (auto [newInstanceAlias, oldInstance] : newInstanceAliasToOldInstanceMap) for (auto [newInstanceAlias, oldInstance] : newInstanceAliasToOldInstanceMap)
{ {
@ -995,8 +996,35 @@ namespace AzToolsFramework
PrefabDom linkPatchesCopy; PrefabDom linkPatchesCopy;
linkPatchesCopy.CopyFrom(linkPatches->get(), linkPatchesCopy.GetAllocator()); linkPatchesCopy.CopyFrom(linkPatches->get(), linkPatchesCopy.GetAllocator());
m_prefabSystemComponentInterface->CreateLink( // If the instance was duplicated as part of an ancestor's nested hierarchy, the container's parent patch
commonOwningInstance->get().GetTemplateId(), oldInstance->GetTemplateId(), newInstanceAlias, linkPatchesCopy); // will need to be refreshed to point to the new duplicated parent entity
auto oldInstanceContainerEntityId = oldInstance->GetContainerEntityId();
AZ_Assert(oldInstanceContainerEntityId.IsValid(), "Instance returned invalid Container Entity Id");
AZ::EntityId previousParentEntityId;
AZ::TransformBus::EventResult(previousParentEntityId, oldInstanceContainerEntityId, &AZ::TransformBus::Events::GetParentId);
if (previousParentEntityId.IsValid() && AZStd::find(duplicatedEntityAndInstanceIds.begin(), duplicatedEntityAndInstanceIds.end(), previousParentEntityId))
{
auto oldParentAlias = commonOwningInstance->get().GetEntityAlias(previousParentEntityId);
if (oldParentAlias.has_value() && duplicateEntityAliasMap.contains(oldParentAlias->get()))
{
// Get the dom into a QString for search/replace purposes
rapidjson::StringBuffer buffer;
rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
linkPatchesCopy.Accept(writer);
QString linkPatchesString(buffer.GetString());
ReplaceOldAliases(linkPatchesString, oldParentAlias->get(), duplicateEntityAliasMap[oldParentAlias->get()]);
linkPatchesCopy.Parse(linkPatchesString.toUtf8().constData());
}
}
PrefabUndoHelpers::CreateLink(
oldInstance->GetTemplateId(), commonOwningInstance->get().GetTemplateId(),
AZStd::move(linkPatchesCopy), newInstanceAlias, undoBatch.GetUndoBatch());
} }
// Select the duplicated entities/instances // Select the duplicated entities/instances
@ -1211,25 +1239,23 @@ namespace AzToolsFramework
const auto instanceTemplateId = instancePtr->GetTemplateId(); const auto instanceTemplateId = instancePtr->GetTemplateId();
auto parentContainerEntityId = parentInstance.GetContainerEntityId(); auto parentContainerEntityId = parentInstance.GetContainerEntityId();
instancePtr->GetNestedInstances(
[&](AZStd::unique_ptr<Instance>& nestedInstancePtr)
{
//get previous link patch
auto linkRef = m_prefabSystemComponentInterface->FindLink(nestedInstancePtr->GetLinkId());
PrefabDomValueReference linkPatches = linkRef->get().GetLinkPatches();
AZ_Assert(
linkPatches.has_value(), "Unable to get patches on link with id '%llu' during prefab creation.",
nestedInstancePtr->GetLinkId());
PrefabDom linkPatchesCopy; instancePtr->DetachNestedInstances(
linkPatchesCopy.CopyFrom(linkPatches->get(), linkPatchesCopy.GetAllocator()); [&](AZStd::unique_ptr<Instance> detachedNestedInstance)
{
PrefabDom& nestedInstanceTemplateDom =
m_prefabSystemComponentInterface->FindTemplateDom(detachedNestedInstance->GetTemplateId());
RemoveLink(nestedInstancePtr, instanceTemplateId, undoBatch.GetUndoBatch()); Instance& nestedInstanceUnderNewParent = parentInstance.AddInstance(AZStd::move(detachedNestedInstance));
UpdateLinkPatchesWithNewEntityAliases(linkPatchesCopy, oldEntityAliases, parentInstance); PrefabDom nestedInstanceDomUnderNewParent;
m_instanceToTemplateInterface->GenerateDomForInstance(
nestedInstanceDomUnderNewParent, nestedInstanceUnderNewParent);
PrefabDom reparentPatch;
m_instanceToTemplateInterface->GeneratePatch(
reparentPatch, nestedInstanceTemplateDom, nestedInstanceDomUnderNewParent);
CreateLink(*nestedInstancePtr, parentTemplateId, undoBatch.GetUndoBatch(), CreateLink(nestedInstanceUnderNewParent, parentTemplateId, undoBatch.GetUndoBatch(), AZStd::move(reparentPatch), true);
AZStd::move(linkPatchesCopy), true);
}); });
} }
@ -1509,14 +1535,13 @@ namespace AzToolsFramework
void PrefabPublicHandler::DuplicateNestedEntitiesInInstance(Instance& commonOwningInstance, void PrefabPublicHandler::DuplicateNestedEntitiesInInstance(Instance& commonOwningInstance,
const AZStd::vector<AZ::Entity*>& entities, PrefabDom& domToAddDuplicatedEntitiesUnder, const AZStd::vector<AZ::Entity*>& entities, PrefabDom& domToAddDuplicatedEntitiesUnder,
EntityIdList& duplicatedEntityIds) EntityIdList& duplicatedEntityIds, AZStd::unordered_map<EntityAlias, EntityAlias>& oldAliasToNewAliasMap)
{ {
if (entities.empty()) if (entities.empty())
{ {
return; return;
} }
AZStd::unordered_map<EntityAlias, EntityAlias> oldAliasToNewAliasMap;
AZStd::unordered_map<EntityAlias, QString> aliasToEntityDomMap; AZStd::unordered_map<EntityAlias, QString> aliasToEntityDomMap;
for (AZ::Entity* entity : entities) for (AZ::Entity* entity : entities)

@ -87,7 +87,7 @@ namespace AzToolsFramework
*/ */
void DuplicateNestedEntitiesInInstance(Instance& commonOwningInstance, void DuplicateNestedEntitiesInInstance(Instance& commonOwningInstance,
const AZStd::vector<AZ::Entity*>& entities, PrefabDom& domToAddDuplicatedEntitiesUnder, const AZStd::vector<AZ::Entity*>& entities, PrefabDom& domToAddDuplicatedEntitiesUnder,
EntityIdList& duplicatedEntityIds); EntityIdList& duplicatedEntityIds, AZStd::unordered_map<EntityAlias, EntityAlias>& oldAliasToNewAliasMap);
/** /**
* Duplicate a list of instances owned by a common owning instance by directly * Duplicate a list of instances owned by a common owning instance by directly
* copying/modifying their entries in the instance DOM * copying/modifying their entries in the instance DOM

@ -13,6 +13,7 @@
#include <AzCore/Math/ToString.h> #include <AzCore/Math/ToString.h>
#include <AzCore/Math/Transform.h> #include <AzCore/Math/Transform.h>
#include <AzCore/Serialization/EditContext.h> #include <AzCore/Serialization/EditContext.h>
#include <AzCore/RTTI/BehaviorContext.h>
#include <AzFramework/Components/NonUniformScaleComponent.h> #include <AzFramework/Components/NonUniformScaleComponent.h>
#include <AzToolsFramework/ToolsComponents/EditorNonUniformScaleComponent.h> #include <AzToolsFramework/ToolsComponents/EditorNonUniformScaleComponent.h>
@ -57,6 +58,13 @@ namespace AzToolsFramework
->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly); ->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly);
} }
} }
if (auto behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context))
{
behaviorContext->ConstantProperty("EditorNonUniformScaleComponentTypeId", BehaviorConstant(EditorNonUniformScaleComponent::RTTI_Type()))
->Attribute(AZ::Script::Attributes::Module, "editor")
->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation);
}
} }
void EditorNonUniformScaleComponent::GetDependentServices(AZ::ComponentDescriptor::DependencyArrayType& dependent) void EditorNonUniformScaleComponent::GetDependentServices(AZ::ComponentDescriptor::DependencyArrayType& dependent)

@ -210,6 +210,19 @@ namespace AzToolsFramework
//! Type to inherit to implement ViewportInteractionRequests. //! Type to inherit to implement ViewportInteractionRequests.
using ViewportInteractionRequestBus = AZ::EBus<ViewportInteractionRequests, ViewportEBusTraits>; using ViewportInteractionRequestBus = AZ::EBus<ViewportInteractionRequests, ViewportEBusTraits>;
//! An interface to notify when changes to viewport settings have happened.
class ViewportSettingNotifications
{
public:
virtual void OnGridSnappingChanged([[maybe_unused]] bool enabled) {}
virtual void OnDrawHelpersChanged([[maybe_unused]] bool enabled) {}
protected:
ViewportSettingNotifications() = default;
};
using ViewportSettingsNotificationBus = AZ::EBus<ViewportSettingNotifications, ViewportEBusTraits>;
//! Requests to freeze the Viewport Input //! Requests to freeze the Viewport Input
//! Added to prevent a bug with the legacy CryEngine Viewport code that would //! Added to prevent a bug with the legacy CryEngine Viewport code that would
//! keep doing raycast tests even when no level is loaded, causing a crash. //! keep doing raycast tests even when no level is loaded, causing a crash.

@ -32,6 +32,33 @@ namespace UnitTest
} }
}; };
// Test component that has a reference to a different entity for use in validating per-instance entity id fixups.
class ComponentWithEntityReference : public AZ::Component
{
public:
AZ_COMPONENT(ComponentWithEntityReference, "{CF5FDE59-86E5-40B6-9272-BBC1C4AFD061}");
void Activate() override
{
}
void Deactivate() override
{
}
static void Reflect(AZ::ReflectContext* reflection)
{
if (auto* serializeContext = azrtti_cast<AZ::SerializeContext*>(reflection))
{
serializeContext->Class<ComponentWithEntityReference, AZ::Component>()
->Field("EntityReference", &ComponentWithEntityReference::m_entityReference)
;
}
}
AZ::EntityId m_entityReference;
};
class SpawnableEntitiesManagerTest : public AllocatorsFixture class SpawnableEntitiesManagerTest : public AllocatorsFixture
{ {
public: public:
@ -42,6 +69,8 @@ namespace UnitTest
m_application = new TestApplication(); m_application = new TestApplication();
AZ::ComponentApplication::Descriptor descriptor; AZ::ComponentApplication::Descriptor descriptor;
m_application->Start(descriptor); m_application->Start(descriptor);
m_application->RegisterComponentDescriptor(ComponentWithEntityReference::CreateDescriptor());
// Without this, the user settings component would attempt to save on finalize/shutdown. Since the file is // Without this, the user settings component would attempt to save on finalize/shutdown. Since the file is
// shared across the whole engine, if multiple tests are run in parallel, the saving could cause a crash // shared across the whole engine, if multiple tests are run in parallel, the saving could cause a crash
// in the unit tests. // in the unit tests.
@ -80,6 +109,7 @@ namespace UnitTest
void FillSpawnable(size_t numElements) void FillSpawnable(size_t numElements)
{ {
AzFramework::Spawnable::EntityList& entities = m_spawnable->GetEntities(); AzFramework::Spawnable::EntityList& entities = m_spawnable->GetEntities();
entities.clear();
entities.reserve(numElements); entities.reserve(numElements);
for (size_t i=0; i<numElements; ++i) for (size_t i=0; i<numElements; ++i)
{ {
@ -123,6 +153,94 @@ namespace UnitTest
} }
} }
enum class EntityReferenceScheme
{
AllReferenceFirst,
AllReferenceLast,
AllReferenceThemselves,
AllReferenceNextCircular,
AllReferencePreviousCircular
};
void CreateEntityReferences(EntityReferenceScheme refScheme)
{
AzFramework::Spawnable::EntityList& entities = m_spawnable->GetEntities();
size_t numElements = entities.size();
for (size_t i = 0; i < numElements; ++i)
{
AZStd::unique_ptr<AZ::Entity>& entity = entities[i];
auto component = entity->CreateComponent<ComponentWithEntityReference>();
switch (refScheme)
{
case EntityReferenceScheme::AllReferenceFirst :
component->m_entityReference = entities[0]->GetId();
break;
case EntityReferenceScheme::AllReferenceLast:
component->m_entityReference = entities[numElements - 1]->GetId();
break;
case EntityReferenceScheme::AllReferenceThemselves:
component->m_entityReference = entities[i]->GetId();
break;
case EntityReferenceScheme::AllReferenceNextCircular:
component->m_entityReference = entities[(i + 1) % numElements]->GetId();
break;
case EntityReferenceScheme::AllReferencePreviousCircular:
component->m_entityReference = entities[(i + numElements - 1) % numElements]->GetId();
break;
}
}
}
// Verify that the entity references are pointing to the correct other entities within the same spawn batch.
// A "spawn batch" is the set of entities produced for each SpawnAllEntities command.
void ValidateEntityReferences(
EntityReferenceScheme refScheme, size_t entitiesPerBatch, AzFramework::SpawnableConstEntityContainerView entities)
{
size_t numElements = entities.size();
for (size_t i = 0; i < numElements; ++i)
{
// Calculate the element offset that's the start of each batch of entities spawned.
size_t curSpawnBatch = i / entitiesPerBatch;
size_t curBatchOffset = curSpawnBatch * entitiesPerBatch;
size_t curBatchIndex = i - curBatchOffset;
const AZ::Entity* const entity = *(entities.begin() + i);
auto component = entity->FindComponent<ComponentWithEntityReference>();
ASSERT_NE(nullptr, component);
AZ::EntityId comparisonId;
// Ids should be local to a batch, so each of these will be compared within a batch of entities, not globally across
// the entire set.
switch (refScheme)
{
case EntityReferenceScheme::AllReferenceFirst:
// Compare against the first entity in each batch
comparisonId = (*(entities.begin() + curBatchOffset))->GetId();
break;
case EntityReferenceScheme::AllReferenceLast:
// Compare against the last entity in each batch
comparisonId = (*(entities.begin() + curBatchOffset + (entitiesPerBatch - 1)))->GetId();
break;
case EntityReferenceScheme::AllReferenceThemselves:
// Compare against itself
comparisonId = entity->GetId();
break;
case EntityReferenceScheme::AllReferenceNextCircular:
// Compare against the next entity in each batch, looping around so that the last entity in the batch should refer
// to the first entity in the batch.
comparisonId = (*(entities.begin() + curBatchOffset + ((curBatchIndex + 1) % entitiesPerBatch)))->GetId();
break;
case EntityReferenceScheme::AllReferencePreviousCircular:
// Compare against the previous entity in each batch, looping around so that the first entity in the batch should refer
// to the last entity in the batch.
comparisonId = (*(entities.begin() + curBatchOffset + ((curBatchIndex + numElements - 1) % entitiesPerBatch)))->GetId();
break;
}
EXPECT_EQ(comparisonId, component->m_entityReference);
}
};
protected: protected:
AZ::Data::Asset<AzFramework::Spawnable>* m_spawnableAsset { nullptr }; AZ::Data::Asset<AzFramework::Spawnable>* m_spawnableAsset { nullptr };
AzFramework::SpawnableEntitiesManager* m_manager { nullptr }; AzFramework::SpawnableEntitiesManager* m_manager { nullptr };
@ -185,6 +303,73 @@ namespace UnitTest
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular); m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
} }
TEST_F(SpawnableEntitiesManagerTest, SpawnAllEntities_AllEntitiesReferenceOtherEntities_EntityIdsAreMappedCorrectly)
{
// This tests that entity id references get mapped correctly in a SpawnAllEntities call whether they're forward referencing
// in the list, backwards referencing, or self-referencing. The circular tests are to ensure the implementation works regardless
// of entity ordering.
for (EntityReferenceScheme refScheme : {
EntityReferenceScheme::AllReferenceFirst, EntityReferenceScheme::AllReferenceLast,
EntityReferenceScheme::AllReferenceThemselves, EntityReferenceScheme::AllReferenceNextCircular,
EntityReferenceScheme::AllReferencePreviousCircular })
{
constexpr size_t NumEntities = 4;
FillSpawnable(NumEntities);
CreateEntityReferences(refScheme);
auto callback = [this, refScheme, NumEntities]
(AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities)
{
ValidateEntityReferences(refScheme, NumEntities, entities);
};
AzFramework::SpawnAllEntitiesOptionalArgs optionalArgs;
optionalArgs.m_completionCallback = AZStd::move(callback);
m_manager->SpawnAllEntities(*m_ticket, AZStd::move(optionalArgs));
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
}
}
TEST_F(SpawnableEntitiesManagerTest, SpawnAllEntities_AllEntitiesReferenceOtherEntities_EntityIdsOnlyReferWithinASingleCall)
{
// This tests that entity id references get mapped correctly with multiple SpawnAllEntities calls. Each call should only map
// the entities to other entities within the same call, regardless of forward or backward mapping.
// For example, suppose entities 1, 2, and 3 refer to 4. In the first SpawnAllEntities call, entities 1-3 will refer to 4.
// In the second SpawnAllEntities call, entities 1-3 will refer to the second 4, not the previously-spawned 4.
for (EntityReferenceScheme refScheme :
{ EntityReferenceScheme::AllReferenceFirst, EntityReferenceScheme::AllReferenceLast,
EntityReferenceScheme::AllReferenceThemselves, EntityReferenceScheme::AllReferenceNextCircular,
EntityReferenceScheme::AllReferencePreviousCircular
})
{
// Make sure we start with a fresh ticket each time, or else each iteration through this loop would continue to build up
// more and more entities.
delete m_ticket;
m_ticket = new AzFramework::EntitySpawnTicket(*m_spawnableAsset);
constexpr size_t NumEntities = 4;
FillSpawnable(NumEntities);
CreateEntityReferences(refScheme);
auto callback = [this, refScheme, NumEntities]
(AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities)
{
ValidateEntityReferences(refScheme, NumEntities, entities);
};
// Spawn twice.
constexpr size_t NumSpawnAllCalls = 2;
for (int spawns = 0; spawns < NumSpawnAllCalls; spawns++)
{
AzFramework::SpawnAllEntitiesOptionalArgs optionalArgs;
optionalArgs.m_completionCallback = AZStd::move(callback);
m_manager->SpawnAllEntities(*m_ticket, AZStd::move(optionalArgs));
}
m_manager->ListEntities(*m_ticket, callback);
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
}
}
TEST_F(SpawnableEntitiesManagerTest, SpawnAllEntities_DeleteTicketBeforeCall_NoCrash) TEST_F(SpawnableEntitiesManagerTest, SpawnAllEntities_DeleteTicketBeforeCall_NoCrash)
{ {
{ {
@ -363,6 +548,180 @@ namespace UnitTest
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular); m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
} }
TEST_F(SpawnableEntitiesManagerTest, SpawnEntities_AllEntitiesReferenceOtherEntities_ForwardReferencesWorkInSingleCall)
{
constexpr EntityReferenceScheme refScheme = EntityReferenceScheme::AllReferenceNextCircular;
constexpr size_t NumEntities = 4;
FillSpawnable(NumEntities);
CreateEntityReferences(refScheme);
auto callback =
[this, refScheme, NumEntities](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities)
{
ValidateEntityReferences(refScheme, NumEntities, entities);
};
// Verify that by default, entities that refer to other entities that haven't been spawned yet have the correct references
// when the spawning all occurs in the same call
m_manager->SpawnEntities(*m_ticket, { 0, 1, 2, 3 });
m_manager->ListEntities(*m_ticket, callback);
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
}
TEST_F(SpawnableEntitiesManagerTest, SpawnEntities_AllEntitiesReferenceOtherEntities_ForwardReferencesWorkAcrossCalls)
{
constexpr EntityReferenceScheme refScheme = EntityReferenceScheme::AllReferenceNextCircular;
constexpr size_t NumEntities = 4;
FillSpawnable(NumEntities);
CreateEntityReferences(refScheme);
auto callback =
[this, refScheme, NumEntities](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities)
{
ValidateEntityReferences(refScheme, NumEntities, entities);
};
// Verify that by default, entities that refer to other entities that haven't been spawned yet have the correct references
// even when the spawning is across multiple calls
m_manager->SpawnEntities(*m_ticket, { 0 });
m_manager->SpawnEntities(*m_ticket, { 1 });
m_manager->SpawnEntities(*m_ticket, { 2 });
m_manager->SpawnEntities(*m_ticket, { 3 });
m_manager->ListEntities(*m_ticket, callback);
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
}
TEST_F(SpawnableEntitiesManagerTest, SpawnEntities_AllEntitiesReferenceOtherEntities_ReferencesPointToFirstOrLatest)
{
// With SpawnEntities, entity references should either refer to the first entity that *will* be spawned, or the last entity
// that *has* been spawned. This test will create entities 0 1 2 3 that all refer to entity 3, and it will create two batches
// of those. In the first batch, they'll forward-reference. In the second batch, they should backward-reference, except for
// the second entity 3, which will now refer to itself as the last one that's been spawned.
constexpr EntityReferenceScheme refScheme = EntityReferenceScheme::AllReferenceLast;
constexpr size_t NumEntities = 4;
FillSpawnable(NumEntities);
CreateEntityReferences(refScheme);
auto callback =
[this, refScheme, NumEntities](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities)
{
size_t numElements = entities.size();
for (size_t i = 0; i < numElements; ++i)
{
const AZ::Entity* const entity = *(entities.begin() + i);
auto component = entity->FindComponent<ComponentWithEntityReference>();
ASSERT_NE(nullptr, component);
AZ::EntityId comparisonId;
if (i < (numElements - 1))
{
// There are two batches of NumEntities elements. Every entity should either forward-reference or backward-reference
// to the last entity of the first batch, except for the very last entity of the second batch, which should reference
// itself.
comparisonId = (*(entities.begin() + (NumEntities- 1)))->GetId();
}
else
{
// The very last entity of the second batch should reference itself because it's now the latest instance of that
// entity to be spawned.
comparisonId = entity->GetId();
}
EXPECT_EQ(comparisonId, component->m_entityReference);
}
};
// Create 2 batches of forward references. In the first batch, entities 0 1 2 will point forward to 3. In the second batch,
// entities 0 1 2 will point *backward* to the first 3, and the second entity 3 will point to itself.
m_manager->SpawnEntities(*m_ticket, { 0, 1, 2, 3 });
m_manager->SpawnEntities(*m_ticket, { 0, 1, 2, 3 });
m_manager->ListEntities(*m_ticket, callback);
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
}
TEST_F(SpawnableEntitiesManagerTest, SpawnEntities_AllEntitiesReferenceOtherEntities_MultipleSpawnsInSameCallReferenceCorrectly)
{
// With SpawnEntities, entity references should either refer to the first entity that *will* be spawned, or the last entity
// that *has* been spawned. This test will create entities 0 1 2 3 that all refer to entity 3, and it will create three sets
// of those in the same call, with the following results:
// - The first 0 1 2 will forward-reference to the first 3
// - The first 3 will reference itself
// - The second 0 1 2 will backwards-reference to the first 3
// - The second 3 will reference itself
// - The third 0 1 2 will backwards-reference to the second 3
// - The third 3 will reference itself
constexpr EntityReferenceScheme refScheme = EntityReferenceScheme::AllReferenceLast;
constexpr size_t NumEntities = 4;
FillSpawnable(NumEntities);
CreateEntityReferences(refScheme);
auto callback =
[this, refScheme, NumEntities](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities)
{
size_t numElements = entities.size();
for (size_t i = 0; i < numElements; ++i)
{
const AZ::Entity* const entity = *(entities.begin() + i);
auto component = entity->FindComponent<ComponentWithEntityReference>();
ASSERT_NE(nullptr, component);
AZ::EntityId comparisonId;
if (i < ((NumEntities * 2) - 1))
{
// The first 7 entities (0 1 2 3 0 1 2) will all refer to the 4th one (1st '3').
comparisonId = (*(entities.begin() + (NumEntities - 1)))->GetId();
}
else if (i < (numElements - 1))
{
// The next 4 entities (3 0 1 2) will all refer to the 8th one (2nd '3').
comparisonId = (*(entities.begin() + ((NumEntities * 2) - 1)))->GetId();
}
else
{
// The very last entity (3) will reference itself (3rd '3').
comparisonId = entity->GetId();
}
EXPECT_EQ(comparisonId, component->m_entityReference);
}
};
// Create the 3 batches of entities 0, 1, 2, 3. The entity references should work as described at the top of the test.
m_manager->SpawnEntities(*m_ticket, { 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3 });
m_manager->ListEntities(*m_ticket, callback);
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
}
TEST_F(SpawnableEntitiesManagerTest, SpawnEntities_AllEntitiesReferenceOtherEntities_OptionalFlagClearsReferenceMap)
{
constexpr EntityReferenceScheme refScheme = EntityReferenceScheme::AllReferenceLast;
constexpr size_t NumEntities = 4;
FillSpawnable(NumEntities);
CreateEntityReferences(refScheme);
auto callback =
[this, refScheme, NumEntities](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities)
{
ValidateEntityReferences(refScheme, NumEntities, entities);
};
// By setting the "referencePreviouslySpawnedEntities" flag to false, the map will get cleared on each call, so in both batches
// the entities will forward-reference to the last entity in the batch. If the flag were true, entities 0 1 2 in the second
// batch would refer backwards to the first entity 3.
AzFramework::SpawnEntitiesOptionalArgs optionalArgsSecondBatch;
optionalArgsSecondBatch.m_completionCallback = AZStd::move(callback);
optionalArgsSecondBatch.m_referencePreviouslySpawnedEntities = false;
m_manager->SpawnEntities(*m_ticket, { 0, 1, 2, 3 }, optionalArgsSecondBatch);
m_manager->SpawnEntities(*m_ticket, { 0, 1, 2, 3 }, AZStd::move(optionalArgsSecondBatch));
m_manager->ListEntities(*m_ticket, callback);
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
}
TEST_F(SpawnableEntitiesManagerTest, SpawnEntities_DeleteTicketBeforeCall_NoCrash) TEST_F(SpawnableEntitiesManagerTest, SpawnEntities_DeleteTicketBeforeCall_NoCrash)
{ {
{ {

@ -56,7 +56,8 @@ namespace AzAssetBrowserRequestHandlerPrivate
using namespace AzToolsFramework; using namespace AzToolsFramework;
using namespace AzToolsFramework::AssetBrowser; using namespace AzToolsFramework::AssetBrowser;
// return true ONLY if we can handle the drop request in the viewport. // return true ONLY if we can handle the drop request in the viewport.
bool CanSpawnEntityForProduct(const ProductAssetBrowserEntry* product) bool CanSpawnEntityForProduct(const ProductAssetBrowserEntry* product,
AZStd::optional<const AZStd::vector<AZ::Data::AssetType>> optionalProductAssetTypes = AZStd::nullopt)
{ {
if (!product) if (!product)
{ {
@ -70,7 +71,6 @@ namespace AzAssetBrowserRequestHandlerPrivate
bool canCreateComponent = false; bool canCreateComponent = false;
AZ::AssetTypeInfoBus::EventResult(canCreateComponent, product->GetAssetType(), &AZ::AssetTypeInfo::CanCreateComponent, product->GetAssetId()); AZ::AssetTypeInfoBus::EventResult(canCreateComponent, product->GetAssetType(), &AZ::AssetTypeInfo::CanCreateComponent, product->GetAssetId());
if (!canCreateComponent) if (!canCreateComponent)
{ {
return false; return false;
@ -78,16 +78,25 @@ namespace AzAssetBrowserRequestHandlerPrivate
AZ::Uuid componentTypeId = AZ::Uuid::CreateNull(); AZ::Uuid componentTypeId = AZ::Uuid::CreateNull();
AZ::AssetTypeInfoBus::EventResult(componentTypeId, product->GetAssetType(), &AZ::AssetTypeInfo::GetComponentTypeId); AZ::AssetTypeInfoBus::EventResult(componentTypeId, product->GetAssetType(), &AZ::AssetTypeInfo::GetComponentTypeId);
if (componentTypeId.IsNull())
if (!componentTypeId.IsNull())
{ {
// we have a component type that handles this asset. // we have a component type that handles this asset.
return true; return false;
}
if (optionalProductAssetTypes.has_value())
{
bool hasConflictingProducts = false;
AZ::AssetTypeInfoBus::EventResult(hasConflictingProducts, product->GetAssetType(), &AZ::AssetTypeInfo::HasConflictingProducts, optionalProductAssetTypes.value());
if (hasConflictingProducts)
{
return false;
}
} }
// additional operations can be added here. // additional operations can be added here.
return false; return true;
} }
void SpawnEntityAtPoint(const ProductAssetBrowserEntry* product, AzQtComponents::ViewportDragContext* viewportDragContext, EntityIdList& spawnList, AzFramework::SliceInstantiationTicket& spawnTicket) void SpawnEntityAtPoint(const ProductAssetBrowserEntry* product, AzQtComponents::ViewportDragContext* viewportDragContext, EntityIdList& spawnList, AzFramework::SliceInstantiationTicket& spawnTicket)
@ -511,9 +520,16 @@ void AzAssetBrowserRequestHandler::Drop(QDropEvent* event, AzQtComponents::DragA
} }
// Handle products // Handle products
AZStd::vector<AZ::Data::AssetType> productAssetTypes;
productAssetTypes.reserve(products.size());
for (const AzToolsFramework::AssetBrowser::ProductAssetBrowserEntry* entry : products)
{
productAssetTypes.emplace_back(entry->GetAssetType());
}
for (const ProductAssetBrowserEntry* product : products) for (const ProductAssetBrowserEntry* product : products)
{ {
if (CanSpawnEntityForProduct(product)) if (CanSpawnEntityForProduct(product, productAssetTypes))
{ {
SpawnEntityAtPoint(product, viewportDragContext, spawnedEntities, spawnTicket); SpawnEntityAtPoint(product, viewportDragContext, spawnedEntities, spawnTicket);
} }

@ -128,6 +128,7 @@ ly_add_target(
Legacy::EditorCore Legacy::EditorCore
RUNTIME_DEPENDENCIES RUNTIME_DEPENDENCIES
Gem::AtomViewportDisplayInfo Gem::AtomViewportDisplayInfo
Legacy::EditorCommon
) )
ly_add_source_properties( ly_add_source_properties(
SOURCES CryEdit.cpp SOURCES CryEdit.cpp

@ -33,13 +33,13 @@ BEGIN
BEGIN BEGIN
BLOCK "040904b0" BLOCK "040904b0"
BEGIN BEGIN
VALUE "CompanyName", "Amazon.com, Inc." VALUE "CompanyName", "Open 3D Foundation"
VALUE "FileDescription", "Lumberyard Editor" VALUE "FileDescription", "O3DE Editor"
VALUE "FileVersion", "0.1.0.1" VALUE "FileVersion", "0.1.0.1"
VALUE "InternalName", "Editor" VALUE "InternalName", "Editor"
VALUE "LegalCopyright", "Portions of this file Copyright (c) Amazon.com, Inc. or its affiliates. All Rights Reserved. Original file Copyright (c) Crytek GMBH. Used under license by Amazon.com, Inc. and its affiliates." VALUE "LegalCopyright", "Portions of this file Copyright (c) Amazon.com, Inc. or its affiliates. All Rights Reserved. Original file Copyright (c) Crytek GMBH. Used under license by Amazon.com, Inc. and its affiliates."
VALUE "OriginalFilename", "Editor.exe" VALUE "OriginalFilename", "Editor.exe"
VALUE "ProductName", "Lumberyard Editor" VALUE "ProductName", "O3DE Editor"
VALUE "ProductVersion", "0.1.0.1" VALUE "ProductVersion", "0.1.0.1"
END END
END END

@ -208,22 +208,9 @@ WelcomeScreenDialog QLabel
margin: 0; margin: 0;
} }
WelcomeScreenDialog QLabel#titleLabel WelcomeScreenDialog QLabel#currentProjectLabel
{ {
font-size: 22px; margin-top: 10px;
line-height: 32px;
}
WelcomeScreenDialog QLabel#bodyLabel
{
font-size: 14px;
line-height: 20px;
}
WelcomeScreenDialog QLabel[fontStyle="sectionTitle"], QLabel#titleLabel[fontStyle="sectionTitle"], QLabel#documentationLink
{
font-size: 16px;
line-height: 24px;
} }
WelcomeScreenDialog QPushButton WelcomeScreenDialog QPushButton
@ -232,36 +219,20 @@ WelcomeScreenDialog QPushButton
line-height: 16px; line-height: 16px;
} }
WelcomeScreenDialog QFrame#viewContainer
{
background-color: transparent;
}
WelcomeScreenDialog QFrame#viewContainer[articleStyle="pinned"]
{
background: rgba(180,139,255,5%);
border: 1px solid #B48BFF;
box-shadow: 0 0 4px 0 rgba(0,0,0,50%);
}
WelcomeScreenDialog QWidget#articleViewContainerRoot WelcomeScreenDialog QWidget#articleViewContainerRoot
{ {
background: #111111; background: #444444;
} }
WelcomeScreenDialog QScrollArea#previewArea WelcomeScreenDialog QWidget#levelViewFTUEContainer
{ {
background-color: transparent; background: #282828;
} }
WelcomeScreenDialog QWidget#articleViewContents QTableWidget#recentLevelTable::item {
{ background-color: rgb(64,64,64);
background-color: transparent; margin-bottom: 4px;
} margin-top: 4px;
WelcomeScreenDialog QFrame#imageFrame
{
background-color: transparent;
} }
/* Particle Editor */ /* Particle Editor */

@ -45,6 +45,7 @@
#include <AzCore/std/algorithm.h> #include <AzCore/std/algorithm.h>
#include <AzCore/Casting/numeric_cast.h> #include <AzCore/Casting/numeric_cast.h>
#include <AzToolsFramework/Viewport/ViewportMessages.h>
AZ_PUSH_DISABLE_DLL_EXPORT_MEMBER_WARNING AZ_PUSH_DISABLE_DLL_EXPORT_MEMBER_WARNING
#include "ui_ViewportTitleDlg.h" #include "ui_ViewportTitleDlg.h"
@ -57,13 +58,16 @@ inline namespace Helpers
{ {
void ToggleHelpers() void ToggleHelpers()
{ {
GetIEditor()->GetDisplaySettings()->DisplayHelpers(!GetIEditor()->GetDisplaySettings()->IsDisplayHelpers()); const bool newValue = !GetIEditor()->GetDisplaySettings()->IsDisplayHelpers();
GetIEditor()->GetDisplaySettings()->DisplayHelpers(newValue);
GetIEditor()->Notify(eNotify_OnDisplayRenderUpdate); GetIEditor()->Notify(eNotify_OnDisplayRenderUpdate);
if (GetIEditor()->GetDisplaySettings()->IsDisplayHelpers() == false) if (newValue == false)
{ {
GetIEditor()->GetObjectManager()->SendEvent(EVENT_HIDE_HELPER); GetIEditor()->GetObjectManager()->SendEvent(EVENT_HIDE_HELPER);
} }
AzToolsFramework::ViewportInteraction::ViewportSettingsNotificationBus::Broadcast(
&AzToolsFramework::ViewportInteraction::ViewportSettingNotifications::OnDrawHelpersChanged, newValue);
} }
bool IsHelpersShown() bool IsHelpersShown()
@ -126,6 +130,7 @@ CViewportTitleDlg::CViewportTitleDlg(QWidget* pParent)
SetupCameraDropdownMenu(); SetupCameraDropdownMenu();
SetupResolutionDropdownMenu(); SetupResolutionDropdownMenu();
SetupViewportInformationMenu(); SetupViewportInformationMenu();
SetupHelpersButton();
SetupOverflowMenu(); SetupOverflowMenu();
Audio::AudioSystemRequestBus::Broadcast(&Audio::AudioSystemRequestBus::Events::PushRequest, gSettings.bMuteAudio ? m_oMuteAudioRequest : m_oUnmuteAudioRequest); Audio::AudioSystemRequestBus::Broadcast(&Audio::AudioSystemRequestBus::Events::PushRequest, gSettings.bMuteAudio ? m_oMuteAudioRequest : m_oUnmuteAudioRequest);
@ -207,15 +212,16 @@ void CViewportTitleDlg::SetupViewportInformationMenu()
} }
void CViewportTitleDlg::SetupHelpersButton()
{
connect(m_ui->m_helpers, &QToolButton::clicked, this, &CViewportTitleDlg::OnToggleHelpers);
m_ui->m_helpers->setChecked(Helpers::IsHelpersShown());
}
void CViewportTitleDlg::SetupOverflowMenu() void CViewportTitleDlg::SetupOverflowMenu()
{ {
// Setup the overflow menu // Setup the overflow menu
QMenu* overFlowMenu = new QMenu(this); QMenu* overFlowMenu = new QMenu(this);
m_debugHelpersAction = new QAction("Debug Helpers", overFlowMenu);
m_debugHelpersAction->setCheckable(true);
m_debugHelpersAction->setChecked(Helpers::IsHelpersShown());
connect(m_debugHelpersAction, &QAction::triggered, this, &CViewportTitleDlg::OnToggleHelpers);
overFlowMenu->addAction(m_debugHelpersAction);
m_audioMuteAction = new QAction("Mute Audio", overFlowMenu); m_audioMuteAction = new QAction("Mute Audio", overFlowMenu);
connect(m_audioMuteAction, &QAction::triggered, this, &CViewportTitleDlg::OnBnClickedMuteAudio); connect(m_audioMuteAction, &QAction::triggered, this, &CViewportTitleDlg::OnBnClickedMuteAudio);
@ -329,7 +335,7 @@ void CViewportTitleDlg::OnMaximize()
void CViewportTitleDlg::OnToggleHelpers() void CViewportTitleDlg::OnToggleHelpers()
{ {
Helpers::ToggleHelpers(); Helpers::ToggleHelpers();
m_debugHelpersAction->setChecked(Helpers::IsHelpersShown()); m_ui->m_helpers->setChecked(Helpers::IsHelpersShown());
} }
void CViewportTitleDlg::SetNoViewportInfo() void CViewportTitleDlg::SetNoViewportInfo()
@ -755,7 +761,7 @@ void CViewportTitleDlg::OnEditorNotifyEvent(EEditorNotifyEvent event)
switch (event) switch (event)
{ {
case eNotify_OnDisplayRenderUpdate: case eNotify_OnDisplayRenderUpdate:
m_debugHelpersAction->setChecked(Helpers::IsHelpersShown()); m_ui->m_helpers->setChecked(Helpers::IsHelpersShown());
break; break;
case eNotify_OnBeginGameMode: case eNotify_OnBeginGameMode:
case eNotify_OnEndGameMode: case eNotify_OnEndGameMode:

@ -102,6 +102,7 @@ protected:
void SetupResolutionDropdownMenu(); void SetupResolutionDropdownMenu();
void SetupViewportInformationMenu(); void SetupViewportInformationMenu();
void SetupOverflowMenu(); void SetupOverflowMenu();
void SetupHelpersButton();
QString m_title; QString m_title;
@ -172,7 +173,6 @@ protected:
QAction* m_normalInformationAction = nullptr; QAction* m_normalInformationAction = nullptr;
QAction* m_fullInformationAction = nullptr; QAction* m_fullInformationAction = nullptr;
QAction* m_compactInformationAction = nullptr; QAction* m_compactInformationAction = nullptr;
QAction* m_debugHelpersAction = nullptr;
QAction* m_audioMuteAction = nullptr; QAction* m_audioMuteAction = nullptr;
QAction* m_enableVRAction = nullptr; QAction* m_enableVRAction = nullptr;
QAction* m_enableGridSnappingAction = nullptr; QAction* m_enableGridSnappingAction = nullptr;

@ -81,6 +81,18 @@
</property> </property>
</widget> </widget>
</item> </item>
<item>
<widget class="QToolButton" name="m_helpers">
<property name="icon">
<iconset>
<normaloff>:/Menu/helpers.svg</normaloff>:/Menu/helpers.svg
</iconset>
</property>
<property name="checkable">
<bool>true</bool>
</property>
</widget>
</item>
<item> <item>
<widget class="QToolButton" name="m_resolutionMenu"> <widget class="QToolButton" name="m_resolutionMenu">
<property name="icon"> <property name="icon">
@ -94,7 +106,7 @@
<widget class="QToolButton" name="m_overflowBtn"> <widget class="QToolButton" name="m_overflowBtn">
<property name="icon"> <property name="icon">
<iconset> <iconset>
<normaloff>:/stylesheet/img/UI20/menu-centered.svg</normaloff>:/stylesheet/img/UI20/menu-centered.svg <normaloff>:/Menu/menu.svg</normaloff>:/Menu/menu.svg
</iconset> </iconset>
</property> </property>
</widget> </widget>

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:263e95489560dac6e5944ef3caba13e598f83ddead324b943ad7735ba015e1a9
size 70727

@ -15,7 +15,8 @@
#include "WelcomeScreenDialog.h" #include "WelcomeScreenDialog.h"
// Qt // Qt
#include <QStringListModel> #include <QTableWidget>
#include <QTableWidgetItem>
#include <QToolTip> #include <QToolTip>
#include <QMenu> #include <QMenu>
#include <QDesktopServices> #include <QDesktopServices>
@ -24,6 +25,7 @@
#include <QScreen> #include <QScreen>
#include <QDesktopWidget> #include <QDesktopWidget>
#include <QTimer> #include <QTimer>
#include <QDateTime>
#include <AzCore/Utils/Utils.h> #include <AzCore/Utils/Utils.h>
@ -74,65 +76,39 @@ static int GetSmallestScreenHeight()
WelcomeScreenDialog::WelcomeScreenDialog(QWidget* pParent) WelcomeScreenDialog::WelcomeScreenDialog(QWidget* pParent)
: QDialog(new WindowDecorationWrapper(WindowDecorationWrapper::OptionAutoAttach | WindowDecorationWrapper::OptionAutoTitleBarButtons, pParent), Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowCloseButtonHint | Qt::WindowTitleHint) : QDialog(new WindowDecorationWrapper(WindowDecorationWrapper::OptionAutoAttach | WindowDecorationWrapper::OptionAutoTitleBarButtons, pParent), Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowCloseButtonHint | Qt::WindowTitleHint)
, ui(new Ui::WelcomeScreenDialog) , ui(new Ui::WelcomeScreenDialog)
, m_pRecentListModel(new QStringListModel(this))
, m_pRecentList(nullptr) , m_pRecentList(nullptr)
{ {
ui->setupUi(this); ui->setupUi(this);
// Make our welcome screen checkboxes appear as toggle switches ui->recentLevelTable->setColumnCount(3);
AzQtComponents::CheckBox::applyToggleSwitchStyle(ui->autoLoadLevel); ui->recentLevelTable->setMouseTracking(true);
AzQtComponents::CheckBox::applyToggleSwitchStyle(ui->showOnStartup); ui->recentLevelTable->setContextMenuPolicy(Qt::CustomContextMenu);
ui->recentLevelTable->horizontalHeader()->hide();
ui->recentLevelTable->verticalHeader()->hide();
ui->recentLevelTable->setSelectionBehavior(QAbstractItemView::SelectRows);
ui->recentLevelTable->setSelectionMode(QAbstractItemView::SingleSelection);
ui->recentLevelTable->setIconSize(QSize(20, 20));
installEventFilter(this);
ui->autoLoadLevel->setChecked(gSettings.bAutoloadLastLevelAtStartup);
ui->showOnStartup->setChecked(!gSettings.bShowDashboardAtStartup);
ui->recentLevelList->setModel(m_pRecentListModel);
ui->recentLevelList->setMouseTracking(true);
ui->recentLevelList->setContextMenuPolicy(Qt::CustomContextMenu);
auto currentProjectButtonMenu = new QMenu();
ui->currentProjectButton->setMenu(currentProjectButtonMenu);
auto projectName = AZ::Utils::GetProjectName(); auto projectName = AZ::Utils::GetProjectName();
ui->currentProjectButton->setText(projectName.c_str()); ui->currentProjectName->setText(projectName.c_str());
ui->currentProjectButton->adjustSize();
ui->currentProjectButton->setMinimumWidth(ui->currentProjectButton->width() + 40); ui->newLevelButton->setDefault(true);
ui->documentationLink->setCursor(Qt::PointingHandCursor); // Hide these buttons until the new functionality is added
ui->documentationLink->installEventFilter(this); ui->gridButton->hide();
ui->objectListButton->hide();
ui->switchProjectButton->hide();
connect(ui->recentLevelList, &QWidget::customContextMenuRequested, this, &WelcomeScreenDialog::OnShowContextMenu); connect(ui->recentLevelTable, &QWidget::customContextMenuRequested, this, &WelcomeScreenDialog::OnShowContextMenu);
connect(ui->recentLevelList, &QListView::entered, this, &WelcomeScreenDialog::OnShowToolTip); connect(ui->recentLevelTable, &QTableWidget::entered, this, &WelcomeScreenDialog::OnShowToolTip);
connect(ui->recentLevelList, &QListView::clicked, this, &WelcomeScreenDialog::OnRecentLevelListItemClicked); connect(ui->recentLevelTable, &QTableWidget::clicked, this, &WelcomeScreenDialog::OnRecentLevelTableItemClicked);
connect(ui->newLevelButton, &QPushButton::clicked, this, &WelcomeScreenDialog::OnNewLevelBtnClicked); connect(ui->newLevelButton, &QPushButton::clicked, this, &WelcomeScreenDialog::OnNewLevelBtnClicked);
connect(ui->levelFileLabel, &QLabel::linkActivated, this, &WelcomeScreenDialog::OnNewLevelLabelClicked);
connect(ui->openLevelButton, &QPushButton::clicked, this, &WelcomeScreenDialog::OnOpenLevelBtnClicked); connect(ui->openLevelButton, &QPushButton::clicked, this, &WelcomeScreenDialog::OnOpenLevelBtnClicked);
connect(ui->newSliceButton, &QPushButton::clicked, this, &WelcomeScreenDialog::OnNewSliceBtnClicked);
connect(ui->openSliceButton, &QPushButton::clicked, this, &WelcomeScreenDialog::OnOpenSliceBtnClicked);
connect(ui->documentationButton, &QPushButton::clicked, this, &WelcomeScreenDialog::OnDocumentationBtnClicked);
connect(ui->showOnStartup, &QCheckBox::clicked, this, &WelcomeScreenDialog::OnShowOnStartupBtnClicked);
connect(ui->autoLoadLevel, &QCheckBox::clicked, this, &WelcomeScreenDialog::OnAutoLoadLevelBtnClicked);
m_manifest = new News::ResourceManifest(
std::bind(&WelcomeScreenDialog::SyncSuccess, this),
std::bind(&WelcomeScreenDialog::SyncFail, this, std::placeholders::_1),
std::bind(&WelcomeScreenDialog::SyncUpdate, this, std::placeholders::_1, std::placeholders::_2));
m_articleViewContainer = new News::ArticleViewContainer(this, *m_manifest);
connect(m_articleViewContainer, &News::ArticleViewContainer::scrolled,
this, &WelcomeScreenDialog::previewAreaScrolled);
ui->articleViewContainerRoot->layout()->addWidget(m_articleViewContainer);
m_manifest->Sync();
#ifndef ENABLE_SLICE_EDITOR
ui->newSliceButton->hide();
ui->openSliceButton->hide();
#endif
// Adjust the height, if need be // Adjust the height, if need be
// Do it in the constructor so that the WindowDecoratorWrapper handles it correctly // Do it in the constructor so that the WindowDecoratorWrapper handles it correctly
int smallestHeight = GetSmallestScreenHeight(); int smallestHeight = GetSmallestScreenHeight();
@ -153,16 +129,10 @@ WelcomeScreenDialog::WelcomeScreenDialog(QWidget* pParent)
WelcomeScreenDialog::~WelcomeScreenDialog() WelcomeScreenDialog::~WelcomeScreenDialog()
{ {
delete ui; delete ui;
delete m_manifest;
} }
void WelcomeScreenDialog::done(int result) void WelcomeScreenDialog::done(int result)
{ {
if (m_waitingOnAsync)
{
m_manifest->Abort();
}
QDialog::done(result); QDialog::done(result);
} }
@ -173,13 +143,11 @@ const QString& WelcomeScreenDialog::GetLevelPath()
bool WelcomeScreenDialog::eventFilter(QObject *watched, QEvent *event) bool WelcomeScreenDialog::eventFilter(QObject *watched, QEvent *event)
{ {
if (watched == ui->documentationLink) if (event->type() == QEvent::Show)
{ {
if (event->type() == QEvent::MouseButtonRelease) ui->recentLevelTable->horizontalHeader()->resizeSection(0, ui->nameLabel->width());
{ ui->recentLevelTable->horizontalHeader()->resizeSection(1, ui->modifiedLabel->width());
OnDocumentationBtnClicked(false); ui->recentLevelTable->horizontalHeader()->resizeSection(2, ui->typeLabel->width());
return true;
}
} }
return QDialog::eventFilter(watched, event); return QDialog::eventFilter(watched, event);
@ -207,6 +175,8 @@ void WelcomeScreenDialog::SetRecentFileList(RecentFileList* pList)
int nCurDir = sCurDir.length(); int nCurDir = sCurDir.length();
int recentListSize = pList->GetSize(); int recentListSize = pList->GetSize();
int currentRow = 0;
ui->recentLevelTable->setRowCount(recentListSize);
for (int i = 0; i < recentListSize; ++i) for (int i = 0; i < recentListSize; ++i)
{ {
const QString& recentFile = pList->m_arrNames[i]; const QString& recentFile = pList->m_arrNames[i];
@ -218,7 +188,7 @@ void WelcomeScreenDialog::SetRecentFileList(RecentFileList* pList)
if (sCurEntryDir.compare(sCurDir, Qt::CaseInsensitive) == 0) if (sCurEntryDir.compare(sCurDir, Qt::CaseInsensitive) == 0)
{ {
QString fullPath = recentFile; QString fullPath = recentFile;
QString name = Path::GetFileName(fullPath); const QString name = Path::GetFile(fullPath);
Path::ConvertSlashToBackSlash(fullPath); Path::ConvertSlashToBackSlash(fullPath);
fullPath = Path::ToUnixPath(fullPath.toLower()); fullPath = Path::ToUnixPath(fullPath.toLower());
@ -226,18 +196,34 @@ void WelcomeScreenDialog::SetRecentFileList(RecentFileList* pList)
if (fullPath.contains(gamePath)) if (fullPath.contains(gamePath))
{ {
m_pRecentListModel->setStringList(m_pRecentListModel->stringList() << QString(name)); if (gSettings.prefabSystem)
{
QIcon icon;
icon.addFile(QString::fromUtf8(":/Level/level.svg"), QSize(), QIcon::Normal, QIcon::Off);
ui->recentLevelTable->setItem(currentRow, 0, new QTableWidgetItem(icon, name));
}
else
{
ui->recentLevelTable->setItem(currentRow, 0, new QTableWidgetItem(name));
}
QFileInfo file(recentFile);
QDateTime dateTime = file.lastModified();
QString date = QLocale::system().toString(dateTime.date(), QLocale::ShortFormat) + " " +
QLocale::system().toString(dateTime.time(), QLocale::LongFormat);
ui->recentLevelTable->setItem(currentRow, 1, new QTableWidgetItem(date));
ui->recentLevelTable->setItem(currentRow++, 2, new QTableWidgetItem(tr("Level")));
m_levels.push_back(std::make_pair(name, recentFile)); m_levels.push_back(std::make_pair(name, recentFile));
} }
} }
} }
} }
} }
ui->recentLevelTable->setRowCount(currentRow);
ui->recentLevelTable->setMinimumHeight(currentRow * ui->recentLevelTable->verticalHeader()->defaultSectionSize());
ui->recentLevelTable->setMaximumHeight(currentRow * ui->recentLevelTable->verticalHeader()->defaultSectionSize());
ui->levelFileLabel->setVisible(currentRow ? false : true);
ui->recentLevelList->setCurrentIndex(QModelIndex()); ui->recentLevelTable->setCurrentIndex(QModelIndex());
int rowSize = ui->recentLevelList->sizeHintForRow(0) + ui->recentLevelList->spacing() * 2;
ui->recentLevelList->setMinimumHeight(m_pRecentListModel->rowCount() * rowSize);
ui->recentLevelList->setMaximumHeight(m_pRecentListModel->rowCount() * rowSize);
} }
@ -245,7 +231,7 @@ void WelcomeScreenDialog::RemoveLevelEntry(int index)
{ {
TNamePathPair levelPath = m_levels[index]; TNamePathPair levelPath = m_levels[index];
m_pRecentListModel->removeRow(index); ui->recentLevelTable->removeRow(index);
m_levels.erase(m_levels.begin() + index); m_levels.erase(m_levels.begin() + index);
@ -284,21 +270,18 @@ void WelcomeScreenDialog::OnShowToolTip(const QModelIndex& index)
{ {
const QString& fullPath = m_levels[index.row()].second; const QString& fullPath = m_levels[index.row()].second;
//TEMPORARY:Begin This can be put back once the main window is in Qt QToolTip::showText(QCursor::pos(), QString("Open level: %1").arg(fullPath));
//QRect itemRect = ui->recentLevelList->visualRect(index);
QToolTip::showText(QCursor::pos(), QString("Open level: %1").arg(fullPath) /*, ui->recentLevelList, itemRect*/);
//TEMPORARY:END
} }
void WelcomeScreenDialog::OnShowContextMenu(const QPoint& pos) void WelcomeScreenDialog::OnShowContextMenu(const QPoint& pos)
{ {
QModelIndex index = ui->recentLevelList->indexAt(pos); QModelIndex index = ui->recentLevelTable->indexAt(pos);
if (index.isValid()) if (index.isValid())
{ {
QString level = m_pRecentListModel->data(index, 0).toString(); QString level = ui->recentLevelTable->itemAt(pos)->text();
QPoint globalPos = ui->recentLevelList->viewport()->mapToGlobal(pos); QPoint globalPos = ui->recentLevelTable->viewport()->mapToGlobal(pos);
QMenu contextMenu; QMenu contextMenu;
contextMenu.addAction(QString("Remove " + level + " from recent list")); contextMenu.addAction(QString("Remove " + level + " from recent list"));
@ -310,13 +293,16 @@ void WelcomeScreenDialog::OnShowContextMenu(const QPoint& pos)
} }
} }
void WelcomeScreenDialog::OnNewLevelBtnClicked([[maybe_unused]] bool checked) void WelcomeScreenDialog::OnNewLevelBtnClicked([[maybe_unused]] bool checked)
{ {
m_levelPath = "new"; m_levelPath = "new";
accept(); accept();
} }
void WelcomeScreenDialog::OnNewLevelLabelClicked([[maybe_unused]] const QString& path)
{
OnNewLevelBtnClicked(true);
}
void WelcomeScreenDialog::OnOpenLevelBtnClicked([[maybe_unused]] bool checked) void WelcomeScreenDialog::OnOpenLevelBtnClicked([[maybe_unused]] bool checked)
{ {
@ -329,27 +315,7 @@ void WelcomeScreenDialog::OnOpenLevelBtnClicked([[maybe_unused]] bool checked)
} }
} }
void WelcomeScreenDialog::OnNewSliceBtnClicked([[maybe_unused]] bool checked) void WelcomeScreenDialog::OnRecentLevelTableItemClicked(const QModelIndex& modelIndex)
{
m_levelPath = "new slice";
accept();
}
void WelcomeScreenDialog::OnOpenSliceBtnClicked(bool)
{
QString fileName = QFileDialog::getOpenFileName(MainWindow::instance(),
tr("Open Slice"),
Path::GetEditingGameDataFolder().c_str(),
tr("Slice (*.slice)"));
if (!fileName.isEmpty())
{
m_levelPath = fileName;
accept();
}
}
void WelcomeScreenDialog::OnRecentLevelListItemClicked(const QModelIndex& modelIndex)
{ {
int index = modelIndex.row(); int index = modelIndex.row();
@ -365,45 +331,6 @@ void WelcomeScreenDialog::OnCloseBtnClicked([[maybe_unused]] bool checked)
accept(); accept();
} }
void WelcomeScreenDialog::OnAutoLoadLevelBtnClicked(bool checked)
{
gSettings.bAutoloadLastLevelAtStartup = checked;
gSettings.Save();
}
void WelcomeScreenDialog::OnShowOnStartupBtnClicked(bool checked)
{
gSettings.bShowDashboardAtStartup = !checked;
gSettings.Save();
if (gSettings.bShowDashboardAtStartup == false)
{
QMessageBox msgBox(AzToolsFramework::GetActiveWindow());
msgBox.setWindowTitle(QObject::tr("Skip the Welcome dialog on startup"));
msgBox.setText(QObject::tr("You may re-enable the Welcome dialog at any time by going to Edit > Editor Settings > Global Preferences in the menu bar."));
msgBox.exec();
}
}
void WelcomeScreenDialog::OnDocumentationBtnClicked([[maybe_unused]] bool checked)
{
QString webLink = tr("https://aws.amazon.com/lumberyard/support/");
QDesktopServices::openUrl(QUrl(webLink));
}
void WelcomeScreenDialog::SyncFail([[maybe_unused]] News::ErrorCode error)
{
m_articleViewContainer->AddErrorMessage();
m_waitingOnAsync = false;
}
void WelcomeScreenDialog::SyncSuccess()
{
m_articleViewContainer->PopulateArticles();
m_waitingOnAsync = false;
}
void WelcomeScreenDialog::previewAreaScrolled() void WelcomeScreenDialog::previewAreaScrolled()
{ {
//this should only be reported once per session //this should only be reported once per session

@ -52,13 +52,9 @@ private:
Ui::WelcomeScreenDialog* ui; Ui::WelcomeScreenDialog* ui;
QString m_levelPath; QString m_levelPath;
QStringListModel* m_pRecentListModel;
TNameFullPathArray m_levels; TNameFullPathArray m_levels;
RecentFileList* m_pRecentList; RecentFileList* m_pRecentList;
News::ResourceManifest* m_manifest = nullptr;
News::ArticleViewContainer* m_articleViewContainer = nullptr;
const char* m_levelExtension = nullptr; const char* m_levelExtension = nullptr;
bool m_waitingOnAsync = true;
bool m_messageScrollReported = false; bool m_messageScrollReported = false;
void RemoveLevelEntry(int index); void RemoveLevelEntry(int index);
@ -66,19 +62,11 @@ private:
void OnShowToolTip(const QModelIndex& index); void OnShowToolTip(const QModelIndex& index);
void OnShowContextMenu(const QPoint& point); void OnShowContextMenu(const QPoint& point);
void OnNewLevelBtnClicked(bool checked); void OnNewLevelBtnClicked(bool checked);
void OnNewLevelLabelClicked(const QString& checked);
void OnOpenLevelBtnClicked(bool checked); void OnOpenLevelBtnClicked(bool checked);
void OnNewSliceBtnClicked(bool checked); void OnRecentLevelTableItemClicked(const QModelIndex& index);
void OnOpenSliceBtnClicked(bool checked);
void OnRecentLevelListItemClicked(const QModelIndex& index);
void OnGettingStartedBtnClicked(bool checked);
void OnTutorialsBtnClicked(bool checked);
void OnDocumentationBtnClicked(bool checked);
void OnForumsBtnClicked(bool checked);
void OnAutoLoadLevelBtnClicked(bool checked);
void OnShowOnStartupBtnClicked(bool checked);
void OnCloseBtnClicked(bool checked); void OnCloseBtnClicked(bool checked);
void SyncUpdate(const QString& /* message */, News::LogType /* logType */) {}
void SyncFail(News::ErrorCode error); void SyncFail(News::ErrorCode error);
void SyncSuccess(); void SyncSuccess();

@ -1,5 +1,5 @@
<RCC> <RCC>
<qresource prefix="WelcomeScreenDialog"> <qresource prefix="WelcomeScreenDialog">
<file>WelcomeScreenDialogHeader.png</file> <file>DefaultActiveProject.png</file>
</qresource> </qresource>
</RCC> </RCC>

@ -2,12 +2,15 @@
<ui version="4.0"> <ui version="4.0">
<class>WelcomeScreenDialog</class> <class>WelcomeScreenDialog</class>
<widget class="QWidget" name="WelcomeScreenDialog"> <widget class="QWidget" name="WelcomeScreenDialog">
<property name="enabled">
<bool>true</bool>
</property>
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>800</width> <width>945</width>
<height>600</height> <height>639</height>
</rect> </rect>
</property> </property>
<property name="sizePolicy"> <property name="sizePolicy">
@ -18,21 +21,21 @@
</property> </property>
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>800</width> <width>945</width>
<height>600</height> <height>639</height>
</size> </size>
</property> </property>
<property name="maximumSize"> <property name="maximumSize">
<size> <size>
<width>800</width> <width>945</width>
<height>16777215</height> <height>639</height>
</size> </size>
</property> </property>
<property name="focusPolicy"> <property name="focusPolicy">
<enum>Qt::TabFocus</enum> <enum>Qt::TabFocus</enum>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
<string>Welcome to Open 3D Engine</string> <string>Welcome to O3DE</string>
</property> </property>
<property name="styleSheet"> <property name="styleSheet">
<string notr="true"/> <string notr="true"/>
@ -54,127 +57,176 @@
<number>0</number> <number>0</number>
</property> </property>
<item> <item>
<widget class="QWidget" name="projectViewContainer" native="true"> <layout class="QHBoxLayout" name="bodyContainer">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QWidget" name="articleViewContainerRoot" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>0</width> <width>183</width>
<height>36</height> <height>0</height>
</size> </size>
</property> </property>
<property name="maximumSize"> <property name="maximumSize">
<size> <size>
<width>16777215</width> <width>183</width>
<height>36</height> <height>16777215</height>
</size> </size>
</property> </property>
<layout class="QHBoxLayout" name="projectViewContainer_layout" stretch="0,0,1,0,0"> <layout class="QVBoxLayout" name="newsContainerLayout">
<property name="spacing"> <property name="spacing">
<number>10</number> <number>0</number>
</property> </property>
<property name="leftMargin"> <property name="leftMargin">
<number>16</number> <number>0</number>
</property> </property>
<property name="topMargin"> <property name="topMargin">
<number>0</number> <number>0</number>
</property> </property>
<property name="rightMargin"> <property name="rightMargin">
<number>12</number> <number>0</number>
</property> </property>
<property name="bottomMargin"> <property name="bottomMargin">
<number>10</number>
</property>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="leftMargin">
<number>15</number>
</property>
<item>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="spacing">
<number>10</number>
</property>
<property name="leftMargin">
<number>0</number> <number>0</number>
</property> </property>
<item> <item>
<widget class="QLabel" name="currentProjectLabel"> <widget class="QLabel" name="currentProjectLabel">
<property name="sizePolicy"> <property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred"> <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch> <horstretch>0</horstretch>
<verstretch>0</verstretch> <verstretch>0</verstretch>
</sizepolicy> </sizepolicy>
</property> </property>
<property name="text"> <property name="text">
<string>Current project:</string> <string>Active project</string>
</property> </property>
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QPushButton" name="currentProjectButton"> <widget class="QLabel" name="activeProjectIcon">
<property name="text">
<string>Current Project Name</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
<item>
<widget class="Line" name="line_2">
<property name="sizePolicy"> <property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed"> <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch> <horstretch>0</horstretch>
<verstretch>0</verstretch> <verstretch>0</verstretch>
</sizepolicy> </sizepolicy>
</property> </property>
<property name="minimumSize">
<size>
<width>126</width>
<height>167</height>
</size>
</property>
<property name="maximumSize"> <property name="maximumSize">
<size> <size>
<width>16777215</width> <width>126</width>
<height>1</height> <height>167</height>
</size> </size>
</property> </property>
<property name="styleSheet"> <property name="text">
<string notr="true">color: &quot;black&quot;</string> <string/>
</property> </property>
<property name="frameShadow"> <property name="pixmap">
<enum>QFrame::Plain</enum> <pixmap resource="WelcomeScreenDialog.qrc">:/WelcomeScreenDialog/DefaultActiveProject.png</pixmap>
</property> </property>
<property name="lineWidth"> <property name="alignment">
<number>0</number> <set>Qt::AlignCenter</set>
</property> </property>
<property name="orientation"> </widget>
<enum>Qt::Horizontal</enum> </item>
<item>
<widget class="QLabel" name="currentProjectName">
<property name="text">
<string>MyGame</string>
</property> </property>
</widget> </widget>
</item> </item>
</layout>
</item>
</layout>
</item>
<item> <item>
<layout class="QHBoxLayout" name="bodyContainer"> <spacer name="verticalSpacer_3">
<property name="spacing"> <property name="orientation">
<number>0</number> <enum>Qt::Vertical</enum>
</property> </property>
<property name="leftMargin"> <property name="sizeHint" stdset="0">
<number>0</number> <size>
<width>20</width>
<height>40</height>
</size>
</property> </property>
<property name="topMargin"> </spacer>
<number>0</number> </item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<property name="leftMargin">
<number>15</number>
</property> </property>
<property name="rightMargin"> <property name="rightMargin">
<number>0</number> <number>15</number>
</property> </property>
<property name="bottomMargin"> <item>
<number>0</number> <widget class="QPushButton" name="switchProjectButton">
<property name="text">
<string>Switch project...</string>
</property> </property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item> <item>
<widget class="QWidget" name="levelViewFTUEContainer" native="true"> <widget class="QWidget" name="levelViewFTUEContainer" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>0</width> <width>762</width>
<height>0</height> <height>0</height>
</size> </size>
</property> </property>
<property name="maximumSize"> <property name="maximumSize">
<size> <size>
<width>320</width> <width>762</width>
<height>16777215</height> <height>16777215</height>
</size> </size>
</property> </property>
@ -183,13 +235,13 @@
<number>0</number> <number>0</number>
</property> </property>
<property name="leftMargin"> <property name="leftMargin">
<number>0</number> <number>20</number>
</property> </property>
<property name="topMargin"> <property name="topMargin">
<number>0</number> <number>0</number>
</property> </property>
<property name="rightMargin"> <property name="rightMargin">
<number>0</number> <number>20</number>
</property> </property>
<property name="bottomMargin"> <property name="bottomMargin">
<number>0</number> <number>0</number>
@ -227,7 +279,7 @@
</size> </size>
</property> </property>
<property name="text"> <property name="text">
<string>Open or create a level</string> <string>Recent Files</string>
</property> </property>
<property name="indent"> <property name="indent">
<number>-1</number> <number>-1</number>
@ -255,16 +307,6 @@
<property name="bottomMargin"> <property name="bottomMargin">
<number>0</number> <number>0</number>
</property> </property>
<item>
<widget class="QListView" name="recentLevelList">
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
<property name="spacing">
<number>4</number>
</property>
</widget>
</item>
</layout> </layout>
</widget> </widget>
</item> </item>
@ -310,8 +352,26 @@
</property> </property>
<item> <item>
<widget class="QPushButton" name="newLevelButton"> <widget class="QPushButton" name="newLevelButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>156</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>156</width>
<height>16777215</height>
</size>
</property>
<property name="text"> <property name="text">
<string>New level...</string> <string>Create new...</string>
</property> </property>
</widget> </widget>
</item> </item>
@ -333,228 +393,216 @@
</item> </item>
<item> <item>
<widget class="QPushButton" name="openLevelButton"> <widget class="QPushButton" name="openLevelButton">
<property name="minimumSize">
<size>
<width>156</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>156</width>
<height>16777215</height>
</size>
</property>
<property name="text"> <property name="text">
<string>Open level...</string> <string>Open...</string>
</property> </property>
</widget> </widget>
</item> </item>
</layout>
</widget>
</item>
<item> <item>
<spacer name="verticalSpacer_2"> <spacer name="horizontalSpacer_2">
<property name="orientation"> <property name="orientation">
<enum>Qt::Vertical</enum> <enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property> </property>
<property name="sizeHint" stdset="0"> <property name="sizeHint" stdset="0">
<size> <size>
<width>20</width> <width>40</width>
<height>20</height> <height>20</height>
</size> </size>
</property> </property>
</spacer> </spacer>
</item> </item>
<item> <item>
<widget class="QWidget" name="horizontalWidget_2" native="true"> <widget class="QToolButton" name="objectListButton">
<property name="sizePolicy"> <property name="text">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed"> <string>...</string>
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<layout class="QHBoxLayout" name="sliceControlContainer">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property> </property>
<property name="bottomMargin"> <property name="icon">
<number>0</number> <iconset resource="../../../Framework/AzQtComponents/AzQtComponents/Components/resources.qrc">
<normaloff>:/stylesheet/img/UI20/toolbar/Object_list.svg</normaloff>:/stylesheet/img/UI20/toolbar/Object_list.svg</iconset>
</property> </property>
<item> <property name="iconSize">
<widget class="QPushButton" name="newSliceButton"> <size>
<property name="text"> <width>24</width>
<string>New slice...</string> <height>24</height>
</size>
</property> </property>
</widget> </widget>
</item> </item>
<item> <item>
<spacer name="horizontalSpacer_2"> <widget class="QToolButton" name="gridButton">
<property name="orientation"> <property name="text">
<enum>Qt::Horizontal</enum> <string>...</string>
</property> </property>
<property name="sizeType"> <property name="icon">
<enum>QSizePolicy::Fixed</enum> <iconset resource="../../../Framework/AzQtComponents/AzQtComponents/Components/resources.qrc">
<normaloff>:/stylesheet/img/UI20/toolbar/Grid.svg</normaloff>:/stylesheet/img/UI20/toolbar/Grid.svg</iconset>
</property> </property>
<property name="sizeHint" stdset="0"> <property name="iconSize">
<size> <size>
<width>24</width> <width>24</width>
<height>0</height> <height>24</height>
</size> </size>
</property> </property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="openSliceButton">
<property name="text">
<string>Open slice...</string>
</property>
</widget>
</item>
</layout>
</widget> </widget>
</item> </item>
</layout> </layout>
</widget> </widget>
</item> </item>
<item> <item>
<widget class="Line" name="line_4"> <spacer name="verticalSpacer_2">
<property name="maximumSize">
<size>
<width>16777215</width>
<height>1</height>
</size>
</property>
<property name="styleSheet">
<string notr="true">color: &quot;black&quot;</string>
</property>
<property name="frameShadow">
<enum>QFrame::Plain</enum>
</property>
<property name="lineWidth">
<number>0</number>
</property>
<property name="orientation"> <property name="orientation">
<enum>Qt::Horizontal</enum> <enum>Qt::Vertical</enum>
</property> </property>
</widget> <property name="sizeType">
</item> <enum>QSizePolicy::Fixed</enum>
<item alignment="Qt::AlignHCenter">
<widget class="QWidget" name="documentationLinkContainer" native="true">
<property name="minimumSize">
<size>
<width>0</width>
<height>48</height>
</size>
</property> </property>
<property name="maximumSize"> <property name="sizeHint" stdset="0">
<size> <size>
<width>16777215</width> <width>20</width>
<height>48</height> <height>20</height>
</size> </size>
</property> </property>
<layout class="QHBoxLayout" name="horizontalLayout"> </spacer>
<property name="spacing"> </item>
<number>10</number> <item>
</property> <layout class="QHBoxLayout" name="horizontalLayout_3" stretch="2,2,1">
<property name="leftMargin"> <property name="leftMargin">
<number>0</number> <number>6</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property> </property>
<item> <item>
<widget class="QToolButton" name="documentationButton"> <widget class="QLabel" name="nameLabel">
<property name="maximumSize">
<size>
<width>24</width>
<height>24</height>
</size>
</property>
<property name="text"> <property name="text">
<string>info</string> <string>Name</string>
</property>
<property name="icon">
<iconset>
<normaloff>:/stylesheet/img/UI20/Info.svg</normaloff>:/stylesheet/img/UI20/Info.svg</iconset>
</property> </property>
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QLabel" name="documentationLink"> <widget class="QLabel" name="modifiedLabel">
<property name="text"> <property name="text">
<string>Documentation and tutorials</string> <string>Last modified</string>
</property> </property>
<property name="class" stdset="0"> </widget>
<string>link</string> </item>
<item>
<widget class="QLabel" name="typeLabel">
<property name="text">
<string>Type</string>
</property> </property>
</widget> </widget>
</item> </item>
</layout> </layout>
</widget>
</item> </item>
</layout> </layout>
</widget> </widget>
</item> </item>
<item> <item>
<widget class="Line" name="line_3"> <layout class="QVBoxLayout" name="verticalLayout_7" stretch="0,0,0">
<property name="maximumSize"> <property name="leftMargin">
<size> <number>16</number>
<width>1</width>
<height>16777215</height>
</size>
</property> </property>
<property name="styleSheet"> <property name="topMargin">
<string notr="true">color: &quot;black&quot;</string> <number>16</number>
</property> </property>
<property name="frameShadow"> <property name="rightMargin">
<enum>QFrame::Plain</enum> <number>16</number>
</property> </property>
<property name="lineWidth"> <item>
<number>0</number> <widget class="QLabel" name="levelFileLabel">
<property name="enabled">
<bool>true</bool>
</property> </property>
<property name="orientation"> <property name="sizePolicy">
<enum>Qt::Vertical</enum> <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>No level file created yet for this project. &lt;a href=&quot;#&quot;&gt;Create one&lt;/a&gt; now.</string>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
</property> </property>
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QWidget" name="articleViewContainerRoot" native="true"> <widget class="QTableWidget" name="recentLevelTable">
<property name="minimumSize"> <property name="enabled">
<size> <bool>true</bool>
<width>480</width>
<height>0</height>
</size>
</property> </property>
<layout class="QVBoxLayout" name="newsContainerLayout"> <property name="sizePolicy">
<property name="spacing"> <sizepolicy hsizetype="Expanding" vsizetype="MinimumExpanding">
<number>0</number> <horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property> </property>
<property name="leftMargin"> <property name="styleSheet">
<number>0</number> <string notr="true"/>
</property> </property>
<property name="topMargin"> <property name="horizontalScrollBarPolicy">
<number>0</number> <enum>Qt::ScrollBarAlwaysOff</enum>
</property> </property>
<property name="rightMargin"> <property name="columnCount">
<number>0</number> <number>3</number>
</property>
<attribute name="horizontalHeaderVisible">
<bool>false</bool>
</attribute>
<attribute name="horizontalHeaderStretchLastSection">
<bool>false</bool>
</attribute>
<attribute name="verticalHeaderVisible">
<bool>false</bool>
</attribute>
<attribute name="verticalHeaderMinimumSectionSize">
<number>1</number>
</attribute>
<attribute name="verticalHeaderDefaultSectionSize">
<number>48</number>
</attribute>
<attribute name="verticalHeaderHighlightSections">
<bool>false</bool>
</attribute>
<attribute name="verticalHeaderStretchLastSection">
<bool>false</bool>
</attribute>
<column/>
<column/>
<column/>
</widget>
</item>
<item>
<spacer name="verticalSpacer_4">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property> </property>
<property name="bottomMargin"> <property name="sizeHint" stdset="0">
<number>0</number> <size>
<width>20</width>
<height>40</height>
</size>
</property> </property>
</layout> </spacer>
</widget>
</item> </item>
</layout> </layout>
</item> </item>
<item> <item>
<widget class="Line" name="line"> <widget class="Line" name="line_4">
<property name="maximumSize"> <property name="maximumSize">
<size> <size>
<width>16777215</width> <width>16777215</width>
@ -575,69 +623,16 @@
</property> </property>
</widget> </widget>
</item> </item>
<item alignment="Qt::AlignRight"> </layout>
<widget class="QWidget" name="optionsViewContainer" native="true">
<property name="minimumSize">
<size>
<width>0</width>
<height>36</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>36</height>
</size>
</property>
<layout class="QHBoxLayout" name="optionsViewContainer_layout">
<property name="spacing">
<number>30</number>
</property>
<property name="leftMargin">
<number>16</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>16</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QCheckBox" name="autoLoadLevel">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Auto-load last opened level on startup</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="showOnStartup">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Skip this dialog on startup</string>
</property>
</widget> </widget>
</item> </item>
</layout> </layout>
</widget>
</item> </item>
</layout> </layout>
</widget> </widget>
<resources> <resources>
<include location="WelcomeScreenDialog.qrc"/> <include location="WelcomeScreenDialog.qrc"/>
<include location="../../../Framework/AzQtComponents/AzQtComponents/Components/resources.qrc"/>
</resources> </resources>
<connections/> <connections/>
</ui> </ui>

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:53b846352880d940621b14b1ea9514e0a4c95aa6ead4d00234a98684c061c04f
size 29505

@ -16,7 +16,7 @@
#include <QLineEdit> #include <QLineEdit>
#include <QMessageBox> #include <QMessageBox>
#include <FormLineEditWidget.h> #include <FormLineEditWidget.h>
#include <FormBrowseEditWidget.h> #include <FormFolderBrowseEditWidget.h>
#include <PythonBindingsInterface.h> #include <PythonBindingsInterface.h>
#include <PathValidator.h> #include <PathValidator.h>
@ -46,28 +46,28 @@ namespace O3DE::ProjectManager
m_engineVersion->lineEdit()->setReadOnly(true); m_engineVersion->lineEdit()->setReadOnly(true);
layout->addWidget(m_engineVersion); layout->addWidget(m_engineVersion);
m_thirdParty = new FormBrowseEditWidget(tr("3rd Party Software Folder"), engineInfo.m_thirdPartyPath, this); m_thirdParty = new FormFolderBrowseEditWidget(tr("3rd Party Software Folder"), engineInfo.m_thirdPartyPath, this);
m_thirdParty->lineEdit()->setValidator(new PathValidator(PathValidator::PathMode::ExistingFolder, this)); m_thirdParty->lineEdit()->setValidator(new PathValidator(PathValidator::PathMode::ExistingFolder, this));
m_thirdParty->lineEdit()->setReadOnly(true); m_thirdParty->lineEdit()->setReadOnly(true);
m_thirdParty->setErrorLabelText(tr("Please provide a valid path to a folder that exists")); m_thirdParty->setErrorLabelText(tr("Please provide a valid path to a folder that exists"));
connect(m_thirdParty->lineEdit(), &QLineEdit::textChanged, this, &EngineSettingsScreen::OnTextChanged); connect(m_thirdParty->lineEdit(), &QLineEdit::textChanged, this, &EngineSettingsScreen::OnTextChanged);
layout->addWidget(m_thirdParty); layout->addWidget(m_thirdParty);
m_defaultProjects = new FormBrowseEditWidget(tr("Default Projects Folder"), engineInfo.m_defaultProjectsFolder, this); m_defaultProjects = new FormFolderBrowseEditWidget(tr("Default Projects Folder"), engineInfo.m_defaultProjectsFolder, this);
m_defaultProjects->lineEdit()->setValidator(new PathValidator(PathValidator::PathMode::ExistingFolder, this)); m_defaultProjects->lineEdit()->setValidator(new PathValidator(PathValidator::PathMode::ExistingFolder, this));
m_defaultProjects->lineEdit()->setReadOnly(true); m_defaultProjects->lineEdit()->setReadOnly(true);
m_defaultProjects->setErrorLabelText(tr("Please provide a valid path to a folder that exists")); m_defaultProjects->setErrorLabelText(tr("Please provide a valid path to a folder that exists"));
connect(m_defaultProjects->lineEdit(), &QLineEdit::textChanged, this, &EngineSettingsScreen::OnTextChanged); connect(m_defaultProjects->lineEdit(), &QLineEdit::textChanged, this, &EngineSettingsScreen::OnTextChanged);
layout->addWidget(m_defaultProjects); layout->addWidget(m_defaultProjects);
m_defaultGems = new FormBrowseEditWidget(tr("Default Gems Folder"), engineInfo.m_defaultGemsFolder, this); m_defaultGems = new FormFolderBrowseEditWidget(tr("Default Gems Folder"), engineInfo.m_defaultGemsFolder, this);
m_defaultGems->lineEdit()->setValidator(new PathValidator(PathValidator::PathMode::ExistingFolder, this)); m_defaultGems->lineEdit()->setValidator(new PathValidator(PathValidator::PathMode::ExistingFolder, this));
m_defaultGems->lineEdit()->setReadOnly(true); m_defaultGems->lineEdit()->setReadOnly(true);
m_defaultGems->setErrorLabelText(tr("Please provide a valid path to a folder that exists")); m_defaultGems->setErrorLabelText(tr("Please provide a valid path to a folder that exists"));
connect(m_defaultGems->lineEdit(), &QLineEdit::textChanged, this, &EngineSettingsScreen::OnTextChanged); connect(m_defaultGems->lineEdit(), &QLineEdit::textChanged, this, &EngineSettingsScreen::OnTextChanged);
layout->addWidget(m_defaultGems); layout->addWidget(m_defaultGems);
m_defaultProjectTemplates = new FormBrowseEditWidget(tr("Default Project Templates Folder"), engineInfo.m_defaultTemplatesFolder, this); m_defaultProjectTemplates = new FormFolderBrowseEditWidget(tr("Default Project Templates Folder"), engineInfo.m_defaultTemplatesFolder, this);
m_defaultProjectTemplates->lineEdit()->setValidator(new PathValidator(PathValidator::PathMode::ExistingFolder, this)); m_defaultProjectTemplates->lineEdit()->setValidator(new PathValidator(PathValidator::PathMode::ExistingFolder, this));
m_defaultProjectTemplates->lineEdit()->setReadOnly(true); m_defaultProjectTemplates->lineEdit()->setReadOnly(true);
m_defaultProjectTemplates->setErrorLabelText(tr("Please provide a valid path to a folder that exists")); m_defaultProjectTemplates->setErrorLabelText(tr("Please provide a valid path to a folder that exists"));

@ -11,13 +11,9 @@
*/ */
#include <FormBrowseEditWidget.h> #include <FormBrowseEditWidget.h>
#include <AzQtComponents/Components/StyledLineEdit.h>
#include <QPushButton> #include <QPushButton>
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QFileDialog>
#include <QLineEdit>
#include <QStandardPaths>
#include <QIcon>
namespace O3DE::ProjectManager namespace O3DE::ProjectManager
{ {
@ -30,20 +26,4 @@ namespace O3DE::ProjectManager
connect(browseButton, &QPushButton::pressed, this, &FormBrowseEditWidget::HandleBrowseButton); connect(browseButton, &QPushButton::pressed, this, &FormBrowseEditWidget::HandleBrowseButton);
m_frameLayout->addWidget(browseButton); m_frameLayout->addWidget(browseButton);
} }
void FormBrowseEditWidget::HandleBrowseButton()
{
QString defaultPath = m_lineEdit->text();
if (defaultPath.isEmpty())
{
defaultPath = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
}
QString directory = QDir::toNativeSeparators(QFileDialog::getExistingDirectory(this, tr("Browse"), defaultPath));
if (!directory.isEmpty())
{
m_lineEdit->setText(directory);
}
}
} // namespace O3DE::ProjectManager } // namespace O3DE::ProjectManager

@ -27,7 +27,7 @@ namespace O3DE::ProjectManager
explicit FormBrowseEditWidget(const QString& labelText, const QString& valueText = "", QWidget* parent = nullptr); explicit FormBrowseEditWidget(const QString& labelText, const QString& valueText = "", QWidget* parent = nullptr);
~FormBrowseEditWidget() = default; ~FormBrowseEditWidget() = default;
private slots: protected slots:
void HandleBrowseButton(); virtual void HandleBrowseButton() = 0;
}; };
} // namespace O3DE::ProjectManager } // namespace O3DE::ProjectManager

@ -0,0 +1,42 @@
/*
* 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 <FormFolderBrowseEditWidget.h>
#include <AzQtComponents/Components/StyledLineEdit.h>
#include <QFileDialog>
#include <QLineEdit>
#include <QStandardPaths>
namespace O3DE::ProjectManager
{
FormFolderBrowseEditWidget::FormFolderBrowseEditWidget(const QString& labelText, const QString& valueText, QWidget* parent)
: FormBrowseEditWidget(labelText, valueText, parent)
{
}
void FormFolderBrowseEditWidget::HandleBrowseButton()
{
QString defaultPath = m_lineEdit->text();
if (defaultPath.isEmpty())
{
defaultPath = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
}
QString directory = QDir::toNativeSeparators(QFileDialog::getExistingDirectory(this, tr("Browse"), defaultPath));
if (!directory.isEmpty())
{
m_lineEdit->setText(directory);
}
}
} // namespace O3DE::ProjectManager

@ -0,0 +1,33 @@
/*
* 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
#if !defined(Q_MOC_RUN)
#include <FormBrowseEditWidget.h>
#endif
namespace O3DE::ProjectManager
{
class FormFolderBrowseEditWidget
: public FormBrowseEditWidget
{
Q_OBJECT
public:
explicit FormFolderBrowseEditWidget(const QString& labelText, const QString& valueText = "", QWidget* parent = nullptr);
~FormFolderBrowseEditWidget() = default;
protected:
void HandleBrowseButton() override;
};
} // namespace O3DE::ProjectManager

@ -0,0 +1,35 @@
/*
* 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 <FormImageBrowseEditWidget.h>
#include <AzQtComponents/Components/StyledLineEdit.h>
#include <QFileDialog>
#include <QLineEdit>
namespace O3DE::ProjectManager
{
FormImageBrowseEditWidget::FormImageBrowseEditWidget(const QString& labelText, const QString& valueText, QWidget* parent)
: FormBrowseEditWidget(labelText, valueText, parent)
{
}
void FormImageBrowseEditWidget::HandleBrowseButton()
{
QString file = QDir::toNativeSeparators(QFileDialog::getOpenFileName(
this, tr("Select Image"), m_lineEdit->text(), tr("PNG (*.png)")));
if (!file.isEmpty())
{
m_lineEdit->setText(file);
}
}
} // namespace O3DE::ProjectManager

@ -0,0 +1,33 @@
/*
* 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
#if !defined(Q_MOC_RUN)
#include <FormBrowseEditWidget.h>
#endif
namespace O3DE::ProjectManager
{
class FormImageBrowseEditWidget
: public FormBrowseEditWidget
{
Q_OBJECT
public:
explicit FormImageBrowseEditWidget(const QString& labelText, const QString& valueText = "", QWidget* parent = nullptr);
~FormImageBrowseEditWidget() = default;
protected:
void HandleBrowseButton() override;
};
} // namespace O3DE::ProjectManager

@ -12,8 +12,8 @@
#include <NewProjectSettingsScreen.h> #include <NewProjectSettingsScreen.h>
#include <PythonBindingsInterface.h> #include <PythonBindingsInterface.h>
#include <FormLineEditWidget.h>
#include <FormBrowseEditWidget.h> #include <FormBrowseEditWidget.h>
#include <FormLineEditWidget.h>
#include <TemplateButtonWidget.h> #include <TemplateButtonWidget.h>
#include <PathValidator.h> #include <PathValidator.h>
#include <EngineInfo.h> #include <EngineInfo.h>

@ -11,6 +11,7 @@
*/ */
#include <ProjectBuilder.h> #include <ProjectBuilder.h>
#include <ProjectManagerDefs.h>
#include <ProjectButtonWidget.h> #include <ProjectButtonWidget.h>
#include <PythonBindingsInterface.h> #include <PythonBindingsInterface.h>
@ -30,8 +31,6 @@ namespace O3DE::ProjectManager
{ {
// 10 Minutes // 10 Minutes
constexpr int MaxBuildTimeMSecs = 600000; constexpr int MaxBuildTimeMSecs = 600000;
static const QString BuildPathPostfix = "windows_vs2019";
static const QString ErrorLogPathPostfix = "CMakeFiles/CMakeProjectBuildError.log";
ProjectBuilderWorker::ProjectBuilderWorker(const ProjectInfo& projectInfo) ProjectBuilderWorker::ProjectBuilderWorker(const ProjectInfo& projectInfo)
: QObject() : QObject()
@ -83,7 +82,7 @@ namespace O3DE::ProjectManager
QStringList QStringList
{ {
"-B", "-B",
QDir(m_projectInfo.m_path).filePath(BuildPathPostfix), QDir(m_projectInfo.m_path).filePath(ProjectBuildPathPostfix),
"-S", "-S",
m_projectInfo.m_path, m_projectInfo.m_path,
"-G", "-G",
@ -123,7 +122,7 @@ namespace O3DE::ProjectManager
QStringList QStringList
{ {
"--build", "--build",
QDir(m_projectInfo.m_path).filePath(BuildPathPostfix), QDir(m_projectInfo.m_path).filePath(ProjectBuildPathPostfix),
"--target", "--target",
m_projectInfo.m_projectName + ".GameLauncher", m_projectInfo.m_projectName + ".GameLauncher",
"Editor", "Editor",
@ -159,8 +158,8 @@ namespace O3DE::ProjectManager
QString ProjectBuilderWorker::LogFilePath() const QString ProjectBuilderWorker::LogFilePath() const
{ {
QDir logFilePath(m_projectInfo.m_path); QDir logFilePath(m_projectInfo.m_path);
logFilePath.cd(BuildPathPostfix); logFilePath.cd(ProjectBuildPathPostfix);
return logFilePath.filePath(ErrorLogPathPostfix); return logFilePath.filePath(ProjectBuildErrorLogPathPostfix);
} }
void ProjectBuilderWorker::WriteErrorLog(const QString& log) void ProjectBuilderWorker::WriteErrorLog(const QString& log)

@ -11,6 +11,7 @@
*/ */
#include <ProjectButtonWidget.h> #include <ProjectButtonWidget.h>
#include <ProjectManagerDefs.h>
#include <AzQtComponents/Utilities/DesktopUtilities.h> #include <AzQtComponents/Utilities/DesktopUtilities.h>
#include <QVBoxLayout> #include <QVBoxLayout>
@ -22,12 +23,11 @@
#include <QMenu> #include <QMenu>
#include <QSpacerItem> #include <QSpacerItem>
#include <QProgressBar> #include <QProgressBar>
#include <QDir>
#include <QFileInfo>
namespace O3DE::ProjectManager namespace O3DE::ProjectManager
{ {
inline constexpr static int s_projectImageWidth = 210;
inline constexpr static int s_projectImageHeight = 280;
LabelButton::LabelButton(QWidget* parent) LabelButton::LabelButton(QWidget* parent)
: QLabel(parent) : QLabel(parent)
{ {
@ -92,11 +92,6 @@ namespace O3DE::ProjectManager
: QFrame(parent) : QFrame(parent)
, m_projectInfo(projectInfo) , m_projectInfo(projectInfo)
{ {
if (m_projectInfo.m_imagePath.isEmpty())
{
m_projectInfo.m_imagePath = ":/DefaultProjectImage.png";
}
BaseSetup(); BaseSetup();
if (processing) if (processing)
{ {
@ -118,20 +113,25 @@ namespace O3DE::ProjectManager
setLayout(vLayout); setLayout(vLayout);
m_projectImageLabel = new LabelButton(this); m_projectImageLabel = new LabelButton(this);
m_projectImageLabel->setFixedSize(s_projectImageWidth, s_projectImageHeight); m_projectImageLabel->setFixedSize(ProjectPreviewImageWidth, ProjectPreviewImageHeight);
m_projectImageLabel->setAlignment(Qt::AlignHCenter | Qt::AlignVCenter); m_projectImageLabel->setAlignment(Qt::AlignHCenter | Qt::AlignVCenter);
connect(m_projectImageLabel, &LabelButton::triggered, [this]() { emit OpenProject(m_projectInfo.m_path); }); connect(m_projectImageLabel, &LabelButton::triggered, [this]() { emit OpenProject(m_projectInfo.m_path); });
vLayout->addWidget(m_projectImageLabel); vLayout->addWidget(m_projectImageLabel);
m_projectImageLabel->setPixmap( QString projectPreviewPath = QDir(m_projectInfo.m_path).filePath(m_projectInfo.m_iconPath);
QPixmap(m_projectInfo.m_imagePath).scaled(m_projectImageLabel->size(), Qt::KeepAspectRatioByExpanding)); QFileInfo doesPreviewExist(projectPreviewPath);
if (!doesPreviewExist.exists() || !doesPreviewExist.isFile())
{
projectPreviewPath = ":/DefaultProjectImage.png";
}
m_projectImageLabel->setPixmap(QPixmap(projectPreviewPath).scaled(m_projectImageLabel->size(), Qt::KeepAspectRatioByExpanding));
m_projectFooter = new QFrame(this); m_projectFooter = new QFrame(this);
QHBoxLayout* hLayout = new QHBoxLayout(); QHBoxLayout* hLayout = new QHBoxLayout();
hLayout->setContentsMargins(0, 0, 0, 0); hLayout->setContentsMargins(0, 0, 0, 0);
m_projectFooter->setLayout(hLayout); m_projectFooter->setLayout(hLayout);
{ {
QLabel* projectNameLabel = new QLabel(m_projectInfo.m_displayName, this); QLabel* projectNameLabel = new QLabel(m_projectInfo.GetProjectDisplayName(), this);
hLayout->addWidget(projectNameLabel); hLayout->addWidget(projectNameLabel);
} }

@ -10,33 +10,74 @@
* *
*/ */
#include "ProjectInfo.h" #include <ProjectInfo.h>
#include <ProjectManagerDefs.h>
#include <QDir>
namespace O3DE::ProjectManager namespace O3DE::ProjectManager
{ {
ProjectInfo::ProjectInfo(const QString& path, const QString& projectName, const QString& displayName, ProjectInfo::ProjectInfo(
const QString& origin, const QString& summary, const QString& imagePath, const QString& backgroundImagePath, const QString& path,
const QString& projectName,
const QString& displayName,
const QString& origin,
const QString& summary,
const QString& iconPath,
const QString& newPreviewImagePath,
const QString& newBackgroundImagePath,
bool needsBuild) bool needsBuild)
: m_path(path) : m_path(path)
, m_projectName(projectName) , m_projectName(projectName)
, m_displayName(displayName) , m_displayName(displayName)
, m_origin(origin) , m_origin(origin)
, m_summary(summary) , m_summary(summary)
, m_imagePath(imagePath) , m_iconPath(iconPath)
, m_backgroundImagePath(backgroundImagePath) , m_newPreviewImagePath(newPreviewImagePath)
, m_newBackgroundImagePath(newBackgroundImagePath)
, m_needsBuild(needsBuild) , m_needsBuild(needsBuild)
{ {
} }
bool ProjectInfo::operator==(const ProjectInfo& rhs) bool ProjectInfo::operator==(const ProjectInfo& rhs) const
{
if (m_path != rhs.m_path)
{
return false;
}
if (m_projectName != rhs.m_projectName)
{ {
return m_path == rhs.m_path return false;
&& m_projectName == rhs.m_projectName }
&& m_imagePath == rhs.m_imagePath if (m_displayName != rhs.m_displayName)
&& m_backgroundImagePath == rhs.m_backgroundImagePath; {
return false;
}
if (m_origin != rhs.m_origin)
{
return false;
}
if (m_summary != rhs.m_summary)
{
return false;
}
if (m_iconPath != rhs.m_iconPath)
{
return false;
}
if (m_newPreviewImagePath != rhs.m_newPreviewImagePath)
{
return false;
}
if (m_newBackgroundImagePath != rhs.m_newBackgroundImagePath)
{
return false;
} }
bool ProjectInfo::operator!=(const ProjectInfo& rhs) return true;
}
bool ProjectInfo::operator!=(const ProjectInfo& rhs) const
{ {
return !operator==(rhs); return !operator==(rhs);
} }
@ -45,4 +86,16 @@ namespace O3DE::ProjectManager
{ {
return !m_path.isEmpty() && !m_projectName.isEmpty(); return !m_path.isEmpty() && !m_projectName.isEmpty();
} }
const QString& ProjectInfo::GetProjectDisplayName() const
{
if (!m_displayName.isEmpty())
{
return m_displayName;
}
else
{
return m_projectName;
}
}
} // namespace O3DE::ProjectManager } // namespace O3DE::ProjectManager

@ -31,14 +31,16 @@ namespace O3DE::ProjectManager
const QString& displayName, const QString& displayName,
const QString& origin, const QString& origin,
const QString& summary, const QString& summary,
const QString& imagePath, const QString& iconPath,
const QString& backgroundImagePath, const QString& newPreviewImagePath,
const QString& newBackgroundImagePath,
bool needsBuild); bool needsBuild);
bool operator==(const ProjectInfo& rhs); bool operator==(const ProjectInfo& rhs) const;
bool operator!=(const ProjectInfo& rhs); bool operator!=(const ProjectInfo& rhs) const;
bool IsValid() const; bool IsValid() const;
const QString& GetProjectDisplayName() const;
// from o3de_manifest.json and o3de_projects.json // from o3de_manifest.json and o3de_projects.json
QString m_path; QString m_path;
@ -48,14 +50,14 @@ namespace O3DE::ProjectManager
QString m_displayName; QString m_displayName;
QString m_origin; QString m_origin;
QString m_summary; QString m_summary;
QString m_iconPath;
QStringList m_userTags; QStringList m_userTags;
// Used on projects home screen // Used as temp variable for replace images
QString m_imagePath; QString m_newPreviewImagePath;
QString m_backgroundImagePath; QString m_newBackgroundImagePath;
// Used in project creation // Used in project creation
bool m_needsBuild = false; //! Does this project need to be built bool m_needsBuild = false; //! Does this project need to be built
}; };
} // namespace O3DE::ProjectManager } // namespace O3DE::ProjectManager

@ -0,0 +1,24 @@
/*
* 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 <QString>
namespace O3DE::ProjectManager
{
inline constexpr static int ProjectPreviewImageWidth = 210;
inline constexpr static int ProjectPreviewImageHeight = 280;
static const QString ProjectBuildPathPostfix = "Windows_VS2019";
static const QString ProjectBuildErrorLogPathPostfix = "CMakeFiles/CMakeProjectBuildError.log";
static const QString ProjectPreviewImagePath = "preview.png";
} // namespace O3DE::ProjectManager

@ -11,7 +11,7 @@
*/ */
#include <ProjectSettingsScreen.h> #include <ProjectSettingsScreen.h>
#include <FormBrowseEditWidget.h> #include <FormFolderBrowseEditWidget.h>
#include <FormLineEditWidget.h> #include <FormLineEditWidget.h>
#include <PathValidator.h> #include <PathValidator.h>
#include <PythonBindingsInterface.h> #include <PythonBindingsInterface.h>
@ -47,7 +47,7 @@ namespace O3DE::ProjectManager
connect(m_projectName->lineEdit(), &QLineEdit::textChanged, this, &ProjectSettingsScreen::ValidateProjectName); connect(m_projectName->lineEdit(), &QLineEdit::textChanged, this, &ProjectSettingsScreen::ValidateProjectName);
m_verticalLayout->addWidget(m_projectName); m_verticalLayout->addWidget(m_projectName);
m_projectPath = new FormBrowseEditWidget(tr("Project Location"), "", this); m_projectPath = new FormFolderBrowseEditWidget(tr("Project Location"), "", this);
m_projectPath->lineEdit()->setReadOnly(true); m_projectPath->lineEdit()->setReadOnly(true);
connect(m_projectPath->lineEdit(), &QLineEdit::textChanged, this, &ProjectSettingsScreen::Validate); connect(m_projectPath->lineEdit(), &QLineEdit::textChanged, this, &ProjectSettingsScreen::Validate);
m_verticalLayout->addWidget(m_projectPath); m_verticalLayout->addWidget(m_projectPath);

@ -32,9 +32,9 @@ namespace O3DE::ProjectManager
~ProjectSettingsScreen() = default; ~ProjectSettingsScreen() = default;
ProjectManagerScreen GetScreenEnum() override; ProjectManagerScreen GetScreenEnum() override;
ProjectInfo GetProjectInfo(); virtual ProjectInfo GetProjectInfo();
bool Validate(); virtual bool Validate();
protected slots: protected slots:
virtual bool ValidateProjectName(); virtual bool ValidateProjectName();

@ -29,11 +29,8 @@ namespace O3DE::ProjectManager
if (!QDir(path).isEmpty()) if (!QDir(path).isEmpty())
{ {
QMessageBox::StandardButton warningResult = QMessageBox::warning( QMessageBox::StandardButton warningResult = QMessageBox::warning(
parent, parent, QObject::tr("Overwrite Directory"),
QObject::tr("Overwrite Directory"), QObject::tr("Directory is not empty! Are you sure you want to overwrite it?"), QMessageBox::No | QMessageBox::Yes);
QObject::tr("Directory is not empty! Are you sure you want to overwrite it?"),
QMessageBox::No | QMessageBox::Yes
);
if (warningResult != QMessageBox::Yes) if (warningResult != QMessageBox::Yes)
{ {
@ -53,14 +50,13 @@ namespace O3DE::ProjectManager
{ {
if (ancestor == descendent) if (ancestor == descendent)
{ {
return false; return true;
} }
descendent.cdUp(); descendent.cdUp();
} } while (!descendent.isRoot());
while (!descendent.isRoot());
return true; return false;
} }
static bool CopyDirectory(const QString& origPath, const QString& newPath) static bool CopyDirectory(const QString& origPath, const QString& newPath)
@ -138,7 +134,7 @@ namespace O3DE::ProjectManager
bool CopyProject(const QString& origPath, const QString& newPath) bool CopyProject(const QString& origPath, const QString& newPath)
{ {
// Disallow copying from or into subdirectory // Disallow copying from or into subdirectory
if (!IsDirectoryDescedent(origPath, newPath) || !IsDirectoryDescedent(newPath, origPath)) if (IsDirectoryDescedent(origPath, newPath) || IsDirectoryDescedent(newPath, origPath))
{ {
return false; return false;
} }
@ -173,20 +169,66 @@ namespace O3DE::ProjectManager
return false; return false;
} }
bool MoveProject(const QString& origPath, const QString& newPath, QWidget* parent) bool MoveProject(QString origPath, QString newPath, QWidget* parent, bool ignoreRegister)
{ {
if (!WarnDirectoryOverwrite(newPath, parent) || !UnregisterProject(origPath)) origPath = QDir::toNativeSeparators(origPath);
newPath = QDir::toNativeSeparators(newPath);
if (!WarnDirectoryOverwrite(newPath, parent) || (!ignoreRegister && !UnregisterProject(origPath)))
{ {
return false; return false;
} }
QDir directory; QDir newDirectory(newPath);
if (directory.rename(origPath, newPath)) if (!newDirectory.removeRecursively())
{ {
return directory.rename(origPath, newPath); return false;
}
if (!newDirectory.rename(origPath, newPath))
{
// Likely failed because trying to move to another partition, try copying
if (!CopyProject(origPath, newPath))
{
return false;
} }
if (!RegisterProject(newPath)) DeleteProjectFiles(origPath, true);
}
if (!ignoreRegister && !RegisterProject(newPath))
{
return false;
}
return true;
}
bool ReplaceFile(const QString& origFile, const QString& newFile, QWidget* parent, bool interactive)
{
QFileInfo original(origFile);
if (original.exists())
{
if (interactive)
{
QMessageBox::StandardButton warningResult = QMessageBox::warning(
parent,
QObject::tr("Overwrite File?"),
QObject::tr("Replacing this will overwrite the current file on disk. Are you sure?"),
QMessageBox::No | QMessageBox::Yes);
if (warningResult == QMessageBox::No)
{
return false;
}
}
if (!QFile::remove(origFile))
{
return false;
}
}
if (!QFile::copy(newFile, origFile))
{ {
return false; return false;
} }

@ -24,7 +24,9 @@ namespace O3DE::ProjectManager
bool CopyProjectDialog(const QString& origPath, QWidget* parent = nullptr); bool CopyProjectDialog(const QString& origPath, QWidget* parent = nullptr);
bool CopyProject(const QString& origPath, const QString& newPath); bool CopyProject(const QString& origPath, const QString& newPath);
bool DeleteProjectFiles(const QString& path, bool force = false); bool DeleteProjectFiles(const QString& path, bool force = false);
bool MoveProject(const QString& origPath, const QString& newPath, QWidget* parent = nullptr); bool MoveProject(QString origPath, QString newPath, QWidget* parent = nullptr, bool ignoreRegister = false);
bool ReplaceFile(const QString& origFile, const QString& newFile, QWidget* parent = nullptr, bool interactive = true);
bool IsVS2019Installed(); bool IsVS2019Installed();

@ -12,6 +12,7 @@
#include <ProjectsScreen.h> #include <ProjectsScreen.h>
#include <ProjectManagerDefs.h>
#include <ProjectButtonWidget.h> #include <ProjectButtonWidget.h>
#include <PythonBindingsInterface.h> #include <PythonBindingsInterface.h>
#include <ProjectUtils.h> #include <ProjectUtils.h>
@ -35,7 +36,6 @@
#include <QSpacerItem> #include <QSpacerItem>
#include <QListWidget> #include <QListWidget>
#include <QListWidgetItem> #include <QListWidgetItem>
#include <QFileInfo>
#include <QScrollArea> #include <QScrollArea>
#include <QStackedWidget> #include <QStackedWidget>
#include <QFrame> #include <QFrame>
@ -218,16 +218,7 @@ namespace O3DE::ProjectManager
ProjectButton* ProjectsScreen::CreateProjectButton(ProjectInfo& project, QLayout* flowLayout, bool processing) ProjectButton* ProjectsScreen::CreateProjectButton(ProjectInfo& project, QLayout* flowLayout, bool processing)
{ {
ProjectButton* projectButton; ProjectButton* projectButton = new ProjectButton(project, this, processing);
QString projectPreviewPath = project.m_path + m_projectPreviewImagePath;
QFileInfo doesPreviewExist(projectPreviewPath);
if (doesPreviewExist.exists() && doesPreviewExist.isFile())
{
project.m_imagePath = projectPreviewPath;
}
projectButton = new ProjectButton(project, this, processing);
flowLayout->addWidget(projectButton); flowLayout->addWidget(projectButton);
@ -438,7 +429,7 @@ namespace O3DE::ProjectManager
{ {
QMessageBox::information(this, QMessageBox::information(this,
tr("Project Should be rebuilt."), tr("Project Should be rebuilt."),
projectInfo.m_projectName + tr(" project likely needs to be rebuilt.")); projectInfo.GetProjectDisplayName() + tr(" project likely needs to be rebuilt."));
} }
} }
@ -499,8 +490,8 @@ namespace O3DE::ProjectManager
{ {
QMessageBox::StandardButton buildProject = QMessageBox::information( QMessageBox::StandardButton buildProject = QMessageBox::information(
this, this,
tr("Building \"%1\"").arg(projectInfo.m_projectName), tr("Building \"%1\"").arg(projectInfo.GetProjectDisplayName()),
tr("Ready to build \"%1\"?").arg(projectInfo.m_projectName), tr("Ready to build \"%1\"?").arg(projectInfo.GetProjectDisplayName()),
QMessageBox::No | QMessageBox::Yes); QMessageBox::No | QMessageBox::Yes);
if (buildProject == QMessageBox::Yes) if (buildProject == QMessageBox::Yes)

@ -80,8 +80,6 @@ namespace O3DE::ProjectManager
QQueue<ProjectInfo> m_buildQueue; QQueue<ProjectInfo> m_buildQueue;
ProjectBuilderController* m_currentBuilder = nullptr; ProjectBuilderController* m_currentBuilder = nullptr;
const QString m_projectPreviewImagePath = "/preview.png";
inline constexpr static int s_contentMargins = 80; inline constexpr static int s_contentMargins = 80;
inline constexpr static int s_spacerSize = 20; inline constexpr static int s_spacerSize = 20;
}; };

@ -12,6 +12,7 @@
#include <PythonBindings.h> #include <PythonBindings.h>
#include <ProjectManagerDefs.h>
// Qt defines slots, which interferes with the use here. // Qt defines slots, which interferes with the use here.
#pragma push_macro("slots") #pragma push_macro("slots")
@ -693,6 +694,7 @@ namespace O3DE::ProjectManager
projectInfo.m_displayName = Py_To_String_Optional(projectData, "display_name", projectInfo.m_projectName); projectInfo.m_displayName = Py_To_String_Optional(projectData, "display_name", projectInfo.m_projectName);
projectInfo.m_origin = Py_To_String_Optional(projectData, "origin", projectInfo.m_origin); projectInfo.m_origin = Py_To_String_Optional(projectData, "origin", projectInfo.m_origin);
projectInfo.m_summary = Py_To_String_Optional(projectData, "summary", projectInfo.m_summary); projectInfo.m_summary = Py_To_String_Optional(projectData, "summary", projectInfo.m_summary);
projectInfo.m_iconPath = Py_To_String_Optional(projectData, "icon", ProjectPreviewImagePath);
if (projectData.contains("user_tags")) if (projectData.contains("user_tags"))
{ {
for (auto tag : projectData["user_tags"]) for (auto tag : projectData["user_tags"])
@ -786,7 +788,7 @@ namespace O3DE::ProjectManager
pybind11::str(projectInfo.m_origin.toStdString()), // new_origin pybind11::str(projectInfo.m_origin.toStdString()), // new_origin
pybind11::str(projectInfo.m_displayName.toStdString()), // new_display pybind11::str(projectInfo.m_displayName.toStdString()), // new_display
pybind11::str(projectInfo.m_summary.toStdString()), // new_summary pybind11::str(projectInfo.m_summary.toStdString()), // new_summary
pybind11::str(projectInfo.m_imagePath.toStdString()), // new_icon pybind11::str(projectInfo.m_iconPath.toStdString()), // new_icon
pybind11::none(), // add_tags not used pybind11::none(), // add_tags not used
pybind11::none(), // remove_tags not used pybind11::none(), // remove_tags not used
pybind11::list(pybind11::cast(newTags))); // replace_tags pybind11::list(pybind11::cast(newTags))); // replace_tags

@ -11,6 +11,7 @@
*/ */
#include <GemCatalog/GemCatalogScreen.h> #include <GemCatalog/GemCatalogScreen.h>
#include <ProjectManagerDefs.h>
#include <PythonBindingsInterface.h> #include <PythonBindingsInterface.h>
#include <ScreenHeaderWidget.h> #include <ScreenHeaderWidget.h>
#include <ScreensCtrl.h> #include <ScreensCtrl.h>
@ -24,6 +25,7 @@
#include <QStackedWidget> #include <QStackedWidget>
#include <QTabWidget> #include <QTabWidget>
#include <QVBoxLayout> #include <QVBoxLayout>
#include <QDir>
namespace O3DE::ProjectManager namespace O3DE::ProjectManager
{ {
@ -100,10 +102,13 @@ namespace O3DE::ProjectManager
} }
void UpdateProjectCtrl::HandleGemsButton() void UpdateProjectCtrl::HandleGemsButton()
{
if (UpdateProjectSettings(true))
{ {
m_stack->setCurrentWidget(m_gemCatalogScreen); m_stack->setCurrentWidget(m_gemCatalogScreen);
Update(); Update();
} }
}
void UpdateProjectCtrl::HandleBackButton() void UpdateProjectCtrl::HandleBackButton()
{ {
@ -113,10 +118,13 @@ namespace O3DE::ProjectManager
Update(); Update();
} }
else else
{
if (UpdateProjectSettings(true))
{ {
emit GotoPreviousScreenRequest(); emit GotoPreviousScreenRequest();
} }
} }
}
void UpdateProjectCtrl::HandleNextButton() void UpdateProjectCtrl::HandleNextButton()
{ {
@ -124,40 +132,11 @@ namespace O3DE::ProjectManager
if (m_stack->currentIndex() == ScreenOrder::Settings && m_updateSettingsScreen) if (m_stack->currentIndex() == ScreenOrder::Settings && m_updateSettingsScreen)
{ {
if (m_updateSettingsScreen) if (!UpdateProjectSettings())
{
if (!m_updateSettingsScreen->Validate())
{
QMessageBox::critical(this, tr("Invalid project settings"), tr("Invalid project settings"));
return;
}
ProjectInfo newProjectSettings = m_updateSettingsScreen->GetProjectInfo();
// Update project if settings changed
if (m_projectInfo != newProjectSettings)
{
auto result = PythonBindingsInterface::Get()->UpdateProject(newProjectSettings);
if (!result.IsSuccess())
{
QMessageBox::critical(this, tr("Project update failed"), tr(result.GetError().c_str()));
return;
}
}
// Check if project path has changed and move it
if (newProjectSettings.m_path != m_projectInfo.m_path)
{
if (!ProjectUtils::MoveProject(m_projectInfo.m_path, newProjectSettings.m_path))
{ {
QMessageBox::critical(this, tr("Project move failed"), tr("Failed to move project."));
return; return;
} }
} }
m_projectInfo = newProjectSettings;
}
}
else if (m_stack->currentIndex() == ScreenOrder::Gems && m_gemCatalogScreen) else if (m_stack->currentIndex() == ScreenOrder::Gems && m_gemCatalogScreen)
{ {
// Enable or disable the gems that got adjusted in the gem catalog and apply them to the given project. // Enable or disable the gems that got adjusted in the gem catalog and apply them to the given project.
@ -190,14 +169,15 @@ namespace O3DE::ProjectManager
{ {
if (m_stack->currentIndex() == ScreenOrder::Gems) if (m_stack->currentIndex() == ScreenOrder::Gems)
{ {
m_header->setTitle(QString(tr("Edit Project Settings: \"%1\"")).arg(m_projectInfo.m_projectName));
m_header->setTitle(QString(tr("Edit Project Settings: \"%1\"")).arg(m_projectInfo.GetProjectDisplayName()));
m_header->setSubTitle(QString(tr("Configure Gems"))); m_header->setSubTitle(QString(tr("Configure Gems")));
m_nextButton->setText(tr("Finalize")); m_nextButton->setText(tr("Save"));
} }
else else
{ {
m_header->setTitle(""); m_header->setTitle("");
m_header->setSubTitle(QString(tr("Edit Project Settings: \"%1\"")).arg(m_projectInfo.m_projectName)); m_header->setSubTitle(QString(tr("Edit Project Settings: \"%1\"")).arg(m_projectInfo.GetProjectDisplayName()));
m_nextButton->setText(tr("Save")); m_nextButton->setText(tr("Save"));
} }
} }
@ -207,4 +187,70 @@ namespace O3DE::ProjectManager
m_updateSettingsScreen->SetProjectInfo(m_projectInfo); m_updateSettingsScreen->SetProjectInfo(m_projectInfo);
} }
bool UpdateProjectCtrl::UpdateProjectSettings(bool shouldConfirm)
{
AZ_Assert(m_updateSettingsScreen, "Update settings screen is nullptr.")
ProjectInfo newProjectSettings = m_updateSettingsScreen->GetProjectInfo();
if (m_projectInfo != newProjectSettings)
{
if (shouldConfirm)
{
QMessageBox::StandardButton warningResult = QMessageBox::warning(
this,
QObject::tr("Unsaved Changes!"),
QObject::tr("Would you like to save your changes to project settings?"),
QMessageBox::No | QMessageBox::Yes
);
if (warningResult == QMessageBox::No)
{
return true;
}
}
if (!m_updateSettingsScreen->Validate())
{
QMessageBox::critical(this, tr("Invalid project settings"), tr("Invalid project settings"));
return false;
}
// Update project if settings changed
{
auto result = PythonBindingsInterface::Get()->UpdateProject(newProjectSettings);
if (!result.IsSuccess())
{
QMessageBox::critical(this, tr("Project update failed"), tr(result.GetError().c_str()));
return false;
}
}
// Check if project path has changed and move it
if (newProjectSettings.m_path != m_projectInfo.m_path)
{
if (!ProjectUtils::MoveProject(m_projectInfo.m_path, newProjectSettings.m_path))
{
QMessageBox::critical(this, tr("Project move failed"), tr("Failed to move project."));
return false;
}
}
if (!newProjectSettings.m_newPreviewImagePath.isEmpty())
{
if (!ProjectUtils::ReplaceFile(
QDir(newProjectSettings.m_path).filePath(newProjectSettings.m_iconPath), newProjectSettings.m_newPreviewImagePath))
{
QMessageBox::critical(this, tr("File replace failed"), tr("Failed to replace project preview image."));
return false;
}
m_updateSettingsScreen->ResetProjectPreviewPath();
}
m_projectInfo = newProjectSettings;
}
return true;
}
} // namespace O3DE::ProjectManager } // namespace O3DE::ProjectManager

@ -46,6 +46,7 @@ namespace O3DE::ProjectManager
private: private:
void Update(); void Update();
void UpdateSettingsScreen(); void UpdateSettingsScreen();
bool UpdateProjectSettings(bool shouldConfirm = false);
enum ScreenOrder enum ScreenOrder
{ {

@ -11,17 +11,43 @@
*/ */
#include <UpdateProjectSettingsScreen.h> #include <UpdateProjectSettingsScreen.h>
#include <FormBrowseEditWidget.h> #include <ProjectManagerDefs.h>
#include <FormImageBrowseEditWidget.h>
#include <FormLineEditWidget.h> #include <FormLineEditWidget.h>
#include <QVBoxLayout>
#include <QLineEdit> #include <QLineEdit>
#include <QDir> #include <QDir>
#include <QLabel>
#include <QFileInfo>
namespace O3DE::ProjectManager namespace O3DE::ProjectManager
{ {
UpdateProjectSettingsScreen::UpdateProjectSettingsScreen(QWidget* parent) UpdateProjectSettingsScreen::UpdateProjectSettingsScreen(QWidget* parent)
: ProjectSettingsScreen(parent) : ProjectSettingsScreen(parent)
, m_userChangedPreview(false)
{ {
m_projectPreview = new FormImageBrowseEditWidget(tr("Project Preview"), "", this);
m_projectPreview->lineEdit()->setReadOnly(true);
connect(m_projectPreview->lineEdit(), &QLineEdit::textChanged, this, &ProjectSettingsScreen::Validate);
connect(m_projectPreview->lineEdit(), &QLineEdit::textChanged, this, &UpdateProjectSettingsScreen::PreviewPathChanged);
connect(m_projectPath->lineEdit(), &QLineEdit::textChanged, this, &UpdateProjectSettingsScreen::UpdateProjectPreviewPath);
m_verticalLayout->addWidget(m_projectPreview);
QVBoxLayout* previewExtrasLayout = new QVBoxLayout(this);
previewExtrasLayout->setAlignment(Qt::AlignLeft);
previewExtrasLayout->setContentsMargins(50, 0, 0, 0);
QLabel* projectPreviewLabel = new QLabel(tr("Select an image (PNG). Minimum %1 x %2 pixels.")
.arg(QString::number(ProjectPreviewImageWidth), QString::number(ProjectPreviewImageHeight)));
previewExtrasLayout->addWidget(projectPreviewLabel);
m_projectPreviewImage = new QLabel(this);
m_projectPreviewImage->setFixedSize(ProjectPreviewImageWidth, ProjectPreviewImageHeight);
m_projectPreviewImage->setAlignment(Qt::AlignHCenter | Qt::AlignVCenter);
previewExtrasLayout->addWidget(m_projectPreviewImage);
m_verticalLayout->addLayout(previewExtrasLayout);
} }
ProjectManagerScreen UpdateProjectSettingsScreen::GetScreenEnum() ProjectManagerScreen UpdateProjectSettingsScreen::GetScreenEnum()
@ -29,10 +55,58 @@ namespace O3DE::ProjectManager
return ProjectManagerScreen::UpdateProjectSettings; return ProjectManagerScreen::UpdateProjectSettings;
} }
ProjectInfo UpdateProjectSettingsScreen::GetProjectInfo()
{
m_projectInfo.m_displayName = m_projectName->lineEdit()->text();
m_projectInfo.m_path = m_projectPath->lineEdit()->text();
if (m_userChangedPreview)
{
m_projectInfo.m_iconPath = ProjectPreviewImagePath;
m_projectInfo.m_newPreviewImagePath = m_projectPreview->lineEdit()->text();
}
return m_projectInfo;
}
void UpdateProjectSettingsScreen::SetProjectInfo(const ProjectInfo& projectInfo) void UpdateProjectSettingsScreen::SetProjectInfo(const ProjectInfo& projectInfo)
{ {
m_projectName->lineEdit()->setText(projectInfo.m_projectName); m_projectInfo = projectInfo;
m_projectName->lineEdit()->setText(projectInfo.GetProjectDisplayName());
m_projectPath->lineEdit()->setText(projectInfo.m_path); m_projectPath->lineEdit()->setText(projectInfo.m_path);
UpdateProjectPreviewPath();
}
void UpdateProjectSettingsScreen::UpdateProjectPreviewPath()
{
if (!m_userChangedPreview)
{
m_projectPreview->lineEdit()->setText(QDir(m_projectPath->lineEdit()->text()).filePath(m_projectInfo.m_iconPath));
// Setting the text sets m_userChangedPreview to true
// Set it back to false because it should only be true when changed by user
m_userChangedPreview = false;
}
}
bool UpdateProjectSettingsScreen::Validate()
{
return ProjectSettingsScreen::Validate() && ValidateProjectPreview();
}
void UpdateProjectSettingsScreen::ResetProjectPreviewPath()
{
m_userChangedPreview = false;
UpdateProjectPreviewPath();
}
void UpdateProjectSettingsScreen::PreviewPathChanged()
{
m_userChangedPreview = true;
// Update with latest image
m_projectPreviewImage->setPixmap(
QPixmap(m_projectPreview->lineEdit()->text()).scaled(m_projectPreviewImage->size(), Qt::KeepAspectRatioByExpanding));
} }
bool UpdateProjectSettingsScreen::ValidateProjectPath() bool UpdateProjectSettingsScreen::ValidateProjectPath()
@ -48,4 +122,39 @@ namespace O3DE::ProjectManager
return projectPathIsValid; return projectPathIsValid;
} }
bool UpdateProjectSettingsScreen::ValidateProjectPreview()
{
bool projectPreviewIsValid = true;
if (m_projectPreview->lineEdit()->text().isEmpty())
{
projectPreviewIsValid = false;
m_projectPreview->setErrorLabelText(tr("Please select a file."));
}
else
{
if (m_userChangedPreview)
{
QFileInfo previewFile(m_projectPreview->lineEdit()->text());
if (!previewFile.exists() || !previewFile.isFile())
{
projectPreviewIsValid = false;
m_projectPreview->setErrorLabelText(tr("Please select a valid png file."));
}
else
{
QString fileType = previewFile.completeSuffix().toLower();
if (fileType != "png")
{
projectPreviewIsValid = false;
m_projectPreview->setErrorLabelText(tr("Please select a png image."));
}
}
}
}
m_projectPreview->setErrorLabelVisible(!projectPreviewIsValid);
return projectPreviewIsValid;
}
} // namespace O3DE::ProjectManager } // namespace O3DE::ProjectManager

@ -15,6 +15,8 @@
#include <ProjectSettingsScreen.h> #include <ProjectSettingsScreen.h>
#endif #endif
QT_FORWARD_DECLARE_CLASS(QLabel)
namespace O3DE::ProjectManager namespace O3DE::ProjectManager
{ {
class UpdateProjectSettingsScreen class UpdateProjectSettingsScreen
@ -25,10 +27,26 @@ namespace O3DE::ProjectManager
~UpdateProjectSettingsScreen() = default; ~UpdateProjectSettingsScreen() = default;
ProjectManagerScreen GetScreenEnum() override; ProjectManagerScreen GetScreenEnum() override;
ProjectInfo GetProjectInfo() override;
void SetProjectInfo(const ProjectInfo& projectInfo); void SetProjectInfo(const ProjectInfo& projectInfo);
bool Validate() override;
void ResetProjectPreviewPath();
public slots:
void UpdateProjectPreviewPath();
void PreviewPathChanged();
protected: protected:
bool ValidateProjectPath() override; bool ValidateProjectPath() override;
virtual bool ValidateProjectPreview();
FormBrowseEditWidget* m_projectPreview;
QLabel* m_projectPreviewImage;
ProjectInfo m_projectInfo;
bool m_userChangedPreview; //! Did the user change the project preview path
}; };
} // namespace O3DE::ProjectManager } // namespace O3DE::ProjectManager

@ -13,6 +13,7 @@
set(FILES set(FILES
Source/Application.h Source/Application.h
Source/Application.cpp Source/Application.cpp
Source/ProjectManagerDefs.h
Source/ScreenDefs.h Source/ScreenDefs.h
Source/ScreenFactory.h Source/ScreenFactory.h
Source/ScreenFactory.cpp Source/ScreenFactory.cpp
@ -25,6 +26,10 @@ set(FILES
Source/FormLineEditWidget.cpp Source/FormLineEditWidget.cpp
Source/FormBrowseEditWidget.h Source/FormBrowseEditWidget.h
Source/FormBrowseEditWidget.cpp Source/FormBrowseEditWidget.cpp
Source/FormFolderBrowseEditWidget.h
Source/FormFolderBrowseEditWidget.cpp
Source/FormImageBrowseEditWidget.h
Source/FormImageBrowseEditWidget.cpp
Source/PathValidator.h Source/PathValidator.h
Source/PathValidator.cpp Source/PathValidator.cpp
Source/ProjectManagerWindow.h Source/ProjectManagerWindow.h

@ -14,4 +14,5 @@ set(FILES
Resources/ProjectManager.qss Resources/ProjectManager.qss
tests/ApplicationTests.cpp tests/ApplicationTests.cpp
tests/main.cpp tests/main.cpp
tests/UtilsTests.cpp
) )

@ -0,0 +1,120 @@
/*
* 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/UnitTest/TestTypes.h>
#include <Application.h>
#include <ProjectUtils.h>
#include <ProjectManager_Test_Traits_Platform.h>
#include <QFile>
#include <QTextStream>
#include <QTimer>
#include <QApplication>
#include <QKeyEvent>
#include <QDir>
namespace O3DE::ProjectManager
{
namespace ProjectUtils
{
class ProjectManagerUtilsTests
: public ::UnitTest::ScopedAllocatorSetupFixture
{
public:
ProjectManagerUtilsTests()
{
m_application = AZStd::make_unique<ProjectManager::Application>();
m_application->Init(false);
QDir dir;
dir.mkdir("ProjectA");
dir.mkdir("ProjectB");
QFile origFile("ProjectA/origFile.txt");
if (origFile.open(QIODevice::ReadWrite))
{
QTextStream stream(&origFile);
stream << "orig" << Qt::endl;
origFile.close();
}
QFile replaceFile("ProjectA/replaceFile.txt");
if (replaceFile.open(QIODevice::ReadWrite))
{
QTextStream stream(&replaceFile);
stream << "replace" << Qt::endl;
replaceFile.close();
}
}
~ProjectManagerUtilsTests()
{
QDir dirA("ProjectA");
dirA.removeRecursively();
QDir dirB("ProjectB");
dirB.removeRecursively();
m_application.reset();
}
AZStd::unique_ptr<ProjectManager::Application> m_application;
};
#if AZ_TRAIT_DISABLE_FAILED_PROJECT_MANAGER_TESTS
TEST_F(ProjectManagerUtilsTests, DISABLED_MoveProject_Succeeds)
#else
TEST_F(ProjectManagerUtilsTests, MoveProject_Succeeds)
#endif // !AZ_TRAIT_DISABLE_FAILED_PROJECT_MANAGER_TESTS
{
EXPECT_TRUE(MoveProject(
QDir::currentPath() + QDir::separator() + "ProjectA",
QDir::currentPath() + QDir::separator() + "ProjectB",
nullptr, true));
QFileInfo origFile("ProjectA/origFile.txt");
EXPECT_TRUE(!origFile.exists());
QFileInfo replaceFile("ProjectA/replaceFile.txt");
EXPECT_TRUE(!replaceFile.exists());
QFileInfo origFileMoved("ProjectB/origFile.txt");
EXPECT_TRUE(origFileMoved.exists() && origFileMoved.isFile());
QFileInfo replaceFileMoved("ProjectB/replaceFile.txt");
EXPECT_TRUE(replaceFileMoved.exists() && replaceFileMoved.isFile());
}
#if AZ_TRAIT_DISABLE_FAILED_PROJECT_MANAGER_TESTS
TEST_F(ProjectManagerUtilsTests, DISABLED_ReplaceFile_Succeeds)
#else
TEST_F(ProjectManagerUtilsTests, ReplaceFile_Succeeds)
#endif // !AZ_TRAIT_DISABLE_FAILED_PROJECT_MANAGER_TESTS
{
EXPECT_TRUE(ReplaceFile("ProjectA/origFile.txt", "ProjectA/replaceFile.txt", nullptr, false));
QFile origFile("ProjectA/origFile.txt");
if (origFile.open(QIODevice::ReadOnly))
{
QTextStream stream(&origFile);
QString line = stream.readLine();
EXPECT_EQ(line, "replace");
origFile.close();
}
else
{
FAIL();
}
}
} // namespace ProjectUtils
} // namespace O3DE::ProjectManager

@ -255,7 +255,6 @@ namespace AZ
{ {
return AZStd::make_pair(animation, anim); return AZStd::make_pair(animation, anim);
} }
Events::ProcessingResult AssImpAnimationImporter::ImportAnimation(AssImpSceneNodeAppendedContext& context) Events::ProcessingResult AssImpAnimationImporter::ImportAnimation(AssImpSceneNodeAppendedContext& context)
{ {
AZ_TraceContext("Importer", "Animation"); AZ_TraceContext("Importer", "Animation");
@ -447,7 +446,22 @@ namespace AZ
return combinedAnimationResult.GetResult(); return combinedAnimationResult.GetResult();
} }
decltype(boneAnimations) parentFillerAnimations;
AZStd::unordered_set<AZStd::string> boneList;
for (int meshIndex = 0; meshIndex < scene->mNumMeshes; ++meshIndex)
{
aiMesh* mesh = scene->mMeshes[meshIndex];
for (int boneIndex = 0; boneIndex < mesh->mNumBones; ++boneIndex)
{
aiBone* bone = mesh->mBones[boneIndex];
boneList.insert(bone->mName.C_Str());
}
}
decltype(boneAnimations) fillerAnimations;
// Go through all the animations and make sure we create animations for bones who's parents don't have an animation // 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) for (auto&& anim : boneAnimations)
@ -459,8 +473,8 @@ namespace AZ
{ {
if (!IsPivotNode(parent->mName)) if (!IsPivotNode(parent->mName))
{ {
if (boneAnimations.find(parent->mName.C_Str()) == boneAnimations.end() && if (!boneAnimations.contains(parent->mName.C_Str()) &&
parentFillerAnimations.find(parent->mName.C_Str()) == parentFillerAnimations.end()) !fillerAnimations.contains(parent->mName.C_Str()))
{ {
// Create 1 key for each type that just copies the current transform // Create 1 key for each type that just copies the current transform
ConsolidatedNodeAnim emptyAnimation; ConsolidatedNodeAnim emptyAnimation;
@ -482,8 +496,8 @@ namespace AZ
emptyAnimation.m_ownedScalingKeys.emplace_back(0, scale); emptyAnimation.m_ownedScalingKeys.emplace_back(0, scale);
emptyAnimation.mScalingKeys = emptyAnimation.m_ownedScalingKeys.data(); emptyAnimation.mScalingKeys = emptyAnimation.m_ownedScalingKeys.data();
parentFillerAnimations.insert( fillerAnimations.insert(AZStd::make_pair(
AZStd::make_pair(parent->mName.C_Str(), AZStd::make_pair(anim.second.first, AZStd::move(emptyAnimation)))); parent->mName.C_Str(), AZStd::make_pair(anim.second.first, AZStd::move(emptyAnimation))));
} }
} }
@ -491,7 +505,7 @@ namespace AZ
} }
} }
boneAnimations.insert(AZStd::make_move_iterator(parentFillerAnimations.begin()), AZStd::make_move_iterator(parentFillerAnimations.end())); boneAnimations.insert(AZStd::make_move_iterator(fillerAnimations.begin()), AZStd::make_move_iterator(fillerAnimations.end()));
auto animItr = boneAnimations.equal_range(currentNode->mName.C_Str()); auto animItr = boneAnimations.equal_range(currentNode->mName.C_Str());

@ -98,6 +98,20 @@ namespace AZ
} }
} }
aiMatrix4x4 CalculateWorldTransform(const aiNode* currentNode)
{
aiMatrix4x4 transform = {};
const aiNode* iteratingNode = currentNode;
while (iteratingNode)
{
transform = iteratingNode->mTransformation * transform;
iteratingNode = iteratingNode->mParent;
}
return transform;
}
Events::ProcessingResult AssImpBoneImporter::ImportBone(AssImpNodeEncounteredContext& context) Events::ProcessingResult AssImpBoneImporter::ImportBone(AssImpNodeEncounteredContext& context)
{ {
AZ_TraceContext("Importer", "Bone"); AZ_TraceContext("Importer", "Bone");
@ -112,11 +126,6 @@ namespace AZ
bool isBone = false; bool isBone = false;
if (NodeParentIsOfType(context.m_scene.GetGraph(), context.m_currentGraphPosition, DataTypes::IBoneData::TYPEINFO_Uuid()))
{
isBone = true;
}
else
{ {
AZStd::unordered_map<AZStd::string, const aiNode*> mainBoneList; AZStd::unordered_map<AZStd::string, const aiNode*> mainBoneList;
AZStd::unordered_map<AZStd::string, const aiBone*> boneLookup; AZStd::unordered_map<AZStd::string, const aiBone*> boneLookup;
@ -171,14 +180,7 @@ namespace AZ
createdBoneData = AZStd::make_shared<SceneData::GraphData::RootBoneData>(); createdBoneData = AZStd::make_shared<SceneData::GraphData::RootBoneData>();
} }
aiMatrix4x4 transform = currentNode->mTransformation; aiMatrix4x4 transform = CalculateWorldTransform(currentNode);
const aiNode* parent = currentNode->mParent;
while (parent)
{
transform = parent->mTransformation * transform;
parent = parent->mParent;
}
SceneAPI::DataTypes::MatrixType globalTransform = AssImpSDKWrapper::AssImpTypeConverter::ToTransform(transform); SceneAPI::DataTypes::MatrixType globalTransform = AssImpSDKWrapper::AssImpTypeConverter::ToTransform(transform);

@ -47,7 +47,8 @@ namespace AZ
} }
} }
void GetAllBones(const aiScene* scene, AZStd::unordered_map<AZStd::string, const aiBone*>& boneLookup) void GetAllBones(
const aiScene* scene, AZStd::unordered_multimap<AZStd::string, const aiBone*>& boneLookup)
{ {
for (unsigned meshIndex = 0; meshIndex < scene->mNumMeshes; ++meshIndex) for (unsigned meshIndex = 0; meshIndex < scene->mNumMeshes; ++meshIndex)
{ {
@ -57,7 +58,7 @@ namespace AZ
{ {
const aiBone* bone = mesh->mBones[boneIndex]; const aiBone* bone = mesh->mBones[boneIndex];
boneLookup[bone->mName.C_Str()] = bone; boneLookup.emplace(bone->mName.C_Str(), bone);
} }
} }
} }
@ -73,41 +74,53 @@ namespace AZ
return Events::ProcessingResult::Ignored; return Events::ProcessingResult::Ignored;
} }
AZStd::unordered_map<AZStd::string, const aiBone*> boneLookup; AZStd::unordered_multimap<AZStd::string, const aiBone*> boneLookup;
GetAllBones(scene, boneLookup); GetAllBones(scene, boneLookup);
auto boneIterator = boneLookup.find(currentNode->mName.C_Str()); auto boneIterator = boneLookup.find(currentNode->mName.C_Str());
const bool isBone = boneIterator != boneLookup.end(); const bool isBone = boneIterator != boneLookup.end();
aiMatrix4x4 combinedTransform; DataTypes::MatrixType localTransform;
if (isBone) if (isBone)
{ {
auto parentNode = currentNode->mParent; AZStd::vector<DataTypes::MatrixType> offsets, inverseOffsets;
auto iteratingNode = currentNode;
aiMatrix4x4 offsetMatrix = boneIterator->second->mOffsetMatrix; while (iteratingNode && boneLookup.count(iteratingNode->mName.C_Str()))
aiMatrix4x4 parentOffset {}; {
AZStd::string name = iteratingNode->mName.C_Str();
auto parentBoneIterator = boneLookup.find(parentNode->mName.C_Str()); auto range = boneLookup.equal_range(name);
if (parentNode && parentBoneIterator != boneLookup.end()) if (range.first != range.second)
{ {
const auto& parentBone = parentBoneIterator->second; // There can be multiple offsetMatrices for a given bone, we're only interested in grabbing the first one
auto boneFirstOffsetMatrix = range.first->second->mOffsetMatrix;
parentOffset = parentBone->mOffsetMatrix; auto azMat = AssImpSDKWrapper::AssImpTypeConverter::ToTransform(boneFirstOffsetMatrix);
offsets.push_back(azMat);
inverseOffsets.push_back(azMat.GetInverseFull());
} }
auto inverseOffset = offsetMatrix; iteratingNode = iteratingNode->mParent;
inverseOffset.Inverse(); }
combinedTransform = parentOffset * inverseOffset; localTransform =
offsets.at(AZ::GetMin(offsets.size()-1, static_cast<decltype(offsets.size())>(1))) // parent bone offset, or if there is no parent, then current node offset
* inverseOffsets.at(inverseOffsets.size() - 1) // Inverse of root bone offset
* offsets.at(offsets.size() - 1) // Root bone offset
* inverseOffsets.at(0); // Inverse of current node offset
} }
else else
{ {
combinedTransform = GetConcatenatedLocalTransform(currentNode); localTransform = AssImpSDKWrapper::AssImpTypeConverter::ToTransform(GetConcatenatedLocalTransform(currentNode));
} }
DataTypes::MatrixType localTransform = AssImpSDKWrapper::AssImpTypeConverter::ToTransform(combinedTransform); // Don't bother adding a node with the identity matrix
if (localTransform == DataTypes::MatrixType::Identity())
{
return Events::ProcessingResult::Ignored;
}
context.m_sourceSceneSystem.SwapTransformForUpAxis(localTransform); context.m_sourceSceneSystem.SwapTransformForUpAxis(localTransform);
context.m_sourceSceneSystem.ConvertUnit(localTransform); context.m_sourceSceneSystem.ConvertUnit(localTransform);

@ -1765,16 +1765,8 @@ namespace LUAEditor
return false; return false;
} }
//name has the full path in it, we need to convert it to an asset name
AZStd::string projectRoot, databaseRoot, databasePath, databaseFile, fileExtension;
if (!AzFramework::StringFunc::AssetDatabasePath::Split(name.toUtf8().data(), &projectRoot, &databaseRoot, &databasePath, &databaseFile, &fileExtension))
{
AZ_Warning("LUAEditorMainWindow", false, AZStd::string::format("<span severity=\"err\">Path is invalid: '%s'</span>", name.toUtf8().data()).c_str());
return false;
}
AzFramework::StringFunc::Path::Split(name.toUtf8().data(), nullptr, &m_lastOpenFilePath); AzFramework::StringFunc::Path::Split(name.toUtf8().data(), nullptr, &m_lastOpenFilePath);
AzFramework::StringFunc::AssetDatabasePath::Join(databasePath.c_str(), databaseFile.c_str(), newAssetName); newAssetName = name.toUtf8().data();
return true; return true;
} }

@ -146,12 +146,12 @@ namespace AWSClientAuth
void AWSCognitoAuthenticationProvider::DeviceCodeGrantSignInAsync() void AWSCognitoAuthenticationProvider::DeviceCodeGrantSignInAsync()
{ {
AZ_Assert(true, "Not supported"); AZ_Assert(false, "Not supported");
} }
void AWSCognitoAuthenticationProvider::DeviceCodeGrantConfirmSignInAsync() void AWSCognitoAuthenticationProvider::DeviceCodeGrantConfirmSignInAsync()
{ {
AZ_Assert(true, "Not supported"); AZ_Assert(false, "Not supported");
} }
void AWSCognitoAuthenticationProvider::RefreshTokensAsync() void AWSCognitoAuthenticationProvider::RefreshTokensAsync()

@ -53,7 +53,7 @@ namespace AWSClientAuth
if (!m_settingsRegistry->MergeSettingsFile(resolvedPath.data(), AZ::SettingsRegistryInterface::Format::JsonMergePatch)) if (!m_settingsRegistry->MergeSettingsFile(resolvedPath.data(), AZ::SettingsRegistryInterface::Format::JsonMergePatch))
{ {
AZ_Error("AuthenticationProviderManager", true, "Error merging settings registry for path: %s", resolvedPath.data()); AZ_Error("AuthenticationProviderManager", false, "Error merging settings registry for path: %s", resolvedPath.data());
return false; return false;
} }
@ -199,7 +199,7 @@ namespace AWSClientAuth
{ {
return enumValue.value(); return enumValue.value();
} }
AZ_Warning("AuthenticationProviderManager", true, "Incorrect string value for enum: %s", name.c_str()); AZ_Warning("AuthenticationProviderManager", false, "Incorrect string value for enum: %s", name.c_str());
return ProviderNameEnum::None; return ProviderNameEnum::None;
} }

@ -39,7 +39,7 @@ namespace AWSClientAuth
{ {
if (!settingsRegistry.lock()->GetObject(m_settings.get(), azrtti_typeid(m_settings.get()), GoogleSettingsPath)) if (!settingsRegistry.lock()->GetObject(m_settings.get(), azrtti_typeid(m_settings.get()), GoogleSettingsPath))
{ {
AZ_Warning("AWSCognitoAuthenticationProvider", true, "Failed to get Google settings object for path %s", GoogleSettingsPath); AZ_Warning("AWSCognitoAuthenticationProvider", false, "Failed to get Google settings object for path %s", GoogleSettingsPath);
return false; return false;
} }
return true; return true;
@ -49,21 +49,21 @@ namespace AWSClientAuth
{ {
AZ_UNUSED(username); AZ_UNUSED(username);
AZ_UNUSED(password); AZ_UNUSED(password);
AZ_Assert(true, "Not supported"); AZ_Assert(false, "Not supported");
} }
void GoogleAuthenticationProvider::PasswordGrantMultiFactorSignInAsync(const AZStd::string& username, const AZStd::string& password) void GoogleAuthenticationProvider::PasswordGrantMultiFactorSignInAsync(const AZStd::string& username, const AZStd::string& password)
{ {
AZ_UNUSED(username); AZ_UNUSED(username);
AZ_UNUSED(password); AZ_UNUSED(password);
AZ_Assert(true, "Not supported"); AZ_Assert(false, "Not supported");
} }
void GoogleAuthenticationProvider::PasswordGrantMultiFactorConfirmSignInAsync(const AZStd::string& username, const AZStd::string& confirmationCode) void GoogleAuthenticationProvider::PasswordGrantMultiFactorConfirmSignInAsync(const AZStd::string& username, const AZStd::string& confirmationCode)
{ {
AZ_UNUSED(username); AZ_UNUSED(username);
AZ_UNUSED(confirmationCode); AZ_UNUSED(confirmationCode);
AZ_Assert(true, "Not supported"); AZ_Assert(false, "Not supported");
} }
// Call Google authentication provider device code end point. // Call Google authentication provider device code end point.

@ -38,7 +38,7 @@ namespace AWSClientAuth
{ {
if (!settingsRegistry.lock()->GetObject(m_settings.get(), azrtti_typeid(m_settings.get()), LwaSettingsPath)) if (!settingsRegistry.lock()->GetObject(m_settings.get(), azrtti_typeid(m_settings.get()), LwaSettingsPath))
{ {
AZ_Warning("AWSCognitoAuthenticationProvider", true, "Failed to get login with Amazon settings object for path %s", LwaSettingsPath); AZ_Warning("AWSCognitoAuthenticationProvider", false, "Failed to get login with Amazon settings object for path %s", LwaSettingsPath);
return false; return false;
} }
return true; return true;
@ -48,21 +48,21 @@ namespace AWSClientAuth
{ {
AZ_UNUSED(username); AZ_UNUSED(username);
AZ_UNUSED(password); AZ_UNUSED(password);
AZ_Assert(true, "Not supported"); AZ_Assert(false, "Not supported");
} }
void LWAAuthenticationProvider::PasswordGrantMultiFactorSignInAsync(const AZStd::string& username, const AZStd::string& password) void LWAAuthenticationProvider::PasswordGrantMultiFactorSignInAsync(const AZStd::string& username, const AZStd::string& password)
{ {
AZ_UNUSED(username); AZ_UNUSED(username);
AZ_UNUSED(password); AZ_UNUSED(password);
AZ_Assert(true, "Not supported"); AZ_Assert(false, "Not supported");
} }
void LWAAuthenticationProvider::PasswordGrantMultiFactorConfirmSignInAsync(const AZStd::string& username, const AZStd::string& confirmationCode) void LWAAuthenticationProvider::PasswordGrantMultiFactorConfirmSignInAsync(const AZStd::string& username, const AZStd::string& confirmationCode)
{ {
AZ_UNUSED(username); AZ_UNUSED(username);
AZ_UNUSED(confirmationCode); AZ_UNUSED(confirmationCode);
AZ_Assert(true, "Not supported"); AZ_Assert(false, "Not supported");
} }
// Call LWA authentication provider device code end point. // Call LWA authentication provider device code end point.

@ -149,7 +149,7 @@ namespace AWSClientAuth
} }
else else
{ {
AZ_Warning("AWSCognitoAuthorizationController", true, "No logins found. Fetching anonymous/unauthenticated credentials"); AZ_Warning("AWSCognitoAuthorizationController", false, "No logins found. Fetching anonymous/unauthenticated credentials");
} }
AZ::JobContext* jobContext = nullptr; AZ::JobContext* jobContext = nullptr;
@ -277,7 +277,7 @@ namespace AWSClientAuth
// Check anonymous credentials as they are optional settings in Cognito Identity pool. // Check anonymous credentials as they are optional settings in Cognito Identity pool.
if (!m_cognitoCachingAnonymousCredentialsProvider->GetAWSCredentials().IsEmpty()) if (!m_cognitoCachingAnonymousCredentialsProvider->GetAWSCredentials().IsEmpty())
{ {
AZ_Warning("AWSCognitoAuthorizationCredentialHandler", true, "No logins found. Using Anonymous credential provider"); AZ_Warning("AWSCognitoAuthorizationCredentialHandler", false, "No logins found. Using Anonymous credential provider");
return m_cognitoCachingAnonymousCredentialsProvider; return m_cognitoCachingAnonymousCredentialsProvider;
} }

@ -257,5 +257,5 @@ TEST_F(AuthenticationProviderManagerScriptCanvasTest, Initialize_Fail_InvalidPat
{ {
AZ_TEST_START_TRACE_SUPPRESSION; AZ_TEST_START_TRACE_SUPPRESSION;
ASSERT_FALSE(m_mockController->Initialize(m_enabledProviderNames, "")); ASSERT_FALSE(m_mockController->Initialize(m_enabledProviderNames, ""));
AZ_TEST_STOP_TRACE_SUPPRESSION(1); AZ_TEST_STOP_TRACE_SUPPRESSION(2);
} }

@ -256,5 +256,5 @@ TEST_F(AuthenticationProviderManagerTest, Initialize_Fail_InvalidPath)
{ {
AZ_TEST_START_TRACE_SUPPRESSION; AZ_TEST_START_TRACE_SUPPRESSION;
ASSERT_FALSE(m_mockController->Initialize(m_enabledProviderNames, "")); ASSERT_FALSE(m_mockController->Initialize(m_enabledProviderNames, ""));
AZ_TEST_STOP_TRACE_SUPPRESSION(1); AZ_TEST_STOP_TRACE_SUPPRESSION(2);
} }

@ -51,6 +51,13 @@ To add additional dependencies, for example other CDK libraries, just add
them to your requirements.txt file and rerun the `..\..\..\Lumberyard\python\pip.cmd install -r .\Gems\AWSClientAuth\cdk\requirements.txt` them to your requirements.txt file and rerun the `..\..\..\Lumberyard\python\pip.cmd install -r .\Gems\AWSClientAuth\cdk\requirements.txt`
command. command.
## Update Authorization Permissions
To give permissions to call AWS resources, please update CognitoIdentityPoolRole class with correct policy statements.
An example IAM permission policy is provided to grant both authenticated and unauthenticated the permission to list S3 buckets in the project.
However, it is expected that developers replace these permissions with those required by your users to use your resources.
## Useful commands ## Useful commands
* `cdk ls` list all stacks in the app * `cdk ls` list all stacks in the app

@ -53,14 +53,17 @@ class CognitoIdentityPoolRole:
} }
}, assume_role_action='sts:AssumeRoleWithWebIdentity')) }, assume_role_action='sts:AssumeRoleWithWebIdentity'))
# basic permissions # The above role is created for developers to add custom permissions that they need to provide authorized
# clients. Developers should update the policy statements below to add their required permissions.
# As an example s3:ListBuckets permissions are provided.
# Note: There must be at least one policy statement here.
stack_statement = iam.PolicyStatement( stack_statement = iam.PolicyStatement(
actions=[ actions=[
's3:ListBuckets' 's3:ListBuckets'
], ],
effect=iam.Effect.ALLOW, effect=iam.Effect.ALLOW,
resources=[ resources=[
'*' f'arn:aws:s3:::{project_name}/*'
], ],
sid=name_utils.format_aws_resource_sid(feature_name, project_name, iam.PolicyStatement.__name__) sid=name_utils.format_aws_resource_sid(feature_name, project_name, iam.PolicyStatement.__name__)
) )

@ -32,12 +32,18 @@ class CognitoUserPoolSMSRole:
name_utils.format_aws_resource_id(feature_name, project_name, env, iam.Role.__name__), name_utils.format_aws_resource_id(feature_name, project_name, env, iam.Role.__name__),
description='Role permissions used by Cognito user pool to send sms', description='Role permissions used by Cognito user pool to send sms',
assumed_by=iam.ServicePrincipal("cognito-idp.amazonaws.com"), assumed_by=iam.ServicePrincipal("cognito-idp.amazonaws.com"),
# Deny all others and then allow only for the current sms role.
inline_policies={ inline_policies={
'SNSRoleInlinePolicy': 'SNSRoleInlinePolicy':
iam.PolicyDocument( iam.PolicyDocument(
statements=[ statements=[
# SMS role will be used by CognitoIDP tp allow to publish to SNS topic owned by CognitoIDP
# team to push a sms.
# Need to use * as the resource name used by CognitoIDP principal service is unknown.
iam.PolicyStatement( iam.PolicyStatement(
actions=["sns:Publish"], resources=["*"] effect=iam.Effect.ALLOW,
actions=['sns:Publish'],
resources=['*']
) )
] ]
) )

@ -144,6 +144,7 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED)
FILES_CMAKE FILES_CMAKE
awscore_editor_tests_files.cmake awscore_editor_tests_files.cmake
${pal_editor_include_dir}/platform_${PAL_PLATFORM_NAME_LOWERCASE}_files.cmake ${pal_editor_include_dir}/platform_${PAL_PLATFORM_NAME_LOWERCASE}_files.cmake
Tests/Editor/Platform/${PAL_PLATFORM_NAME}/awscore_editor_tests_${PAL_PLATFORM_NAME_LOWERCASE}_files.cmake
INCLUDE_DIRECTORIES INCLUDE_DIRECTORIES
PRIVATE PRIVATE
Include/Private Include/Private

@ -46,6 +46,7 @@ namespace AWSCore
void InitializeAWSDocActions(); void InitializeAWSDocActions();
void InitializeAWSGlobalDocsSubMenu(); void InitializeAWSGlobalDocsSubMenu();
void InitializeAWSFeatureGemActions(); void InitializeAWSFeatureGemActions();
void AddSpaceForIcon(QMenu* menu);
// AWSCoreEditorRequestBus interface implementation // AWSCoreEditorRequestBus interface implementation
void SetAWSClientAuthEnabled() override; void SetAWSClientAuthEnabled() override;

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

Loading…
Cancel
Save