Initial commit

This commit is contained in:
alexpete
2021-03-05 11:26:34 -08:00
commit a10351f38d
27091 changed files with 5521199 additions and 0 deletions
@@ -0,0 +1,621 @@
/*
* 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 "GemDescription.h"
#include "GemRegistry.h"
#include <AzCore/std/smart_ptr/make_shared.h>
#include <AzCore/JSON/error/en.h>
#include <AzFramework/StringFunc/StringFunc.h>
// For LinkTypeFromString
#include <unordered_map>
#include <string>
namespace Gems
{
GemDescription::GemDescription()
: m_id(AZ::Uuid::CreateNull())
, m_name()
, m_displayName()
, m_version()
, m_path()
, m_absolutePath()
, m_summary()
, m_iconPath()
, m_tags()
, m_modules()
, m_modulesByType()
, m_engineModuleClass()
, m_gemDependencies()
, m_gameGem(false)
, m_required(false)
, m_engineDependency(nullptr)
{
m_modulesByType.emplace(ModuleDefinition::Type::GameModule);
m_modulesByType.emplace(ModuleDefinition::Type::ServerModule);
m_modulesByType.emplace(ModuleDefinition::Type::EditorModule);
m_modulesByType.emplace(ModuleDefinition::Type::StaticLib);
m_modulesByType.emplace(ModuleDefinition::Type::Builder);
m_modulesByType.emplace(ModuleDefinition::Type::Standalone);
}
GemDescription::GemDescription(const GemDescription& rhs)
: m_id(rhs.m_id)
, m_name(rhs.m_name)
, m_displayName(rhs.m_displayName)
, m_version(rhs.m_version)
, m_path(rhs.m_path)
, m_absolutePath(rhs.m_absolutePath)
, m_summary(rhs.m_summary)
, m_iconPath(rhs.m_iconPath)
, m_tags(rhs.m_tags)
, m_modules(rhs.m_modules)
, m_modulesByType(rhs.m_modulesByType)
, m_engineModuleClass(rhs.m_engineModuleClass)
, m_gemDependencies(rhs.m_gemDependencies)
, m_gameGem(rhs.m_gameGem)
, m_required(rhs.m_required)
, m_engineDependency(rhs.m_engineDependency)
{
}
GemDescription::GemDescription(GemDescription&& rhs)
: m_id(rhs.m_id)
, m_name(AZStd::move(rhs.m_name))
, m_displayName(AZStd::move(rhs.m_displayName))
, m_path(AZStd::move(rhs.m_path))
, m_absolutePath(AZStd::move(rhs.m_absolutePath))
, m_summary(AZStd::move(rhs.m_summary))
, m_iconPath(AZStd::move(rhs.m_iconPath))
, m_tags(AZStd::move(rhs.m_tags))
, m_version(rhs.m_version)
, m_modules(AZStd::move(rhs.m_modules))
, m_modulesByType(AZStd::move(rhs.m_modulesByType))
, m_engineModuleClass(AZStd::move(rhs.m_engineModuleClass))
, m_gemDependencies(AZStd::move(rhs.m_gemDependencies))
, m_gameGem(AZStd::move(rhs.m_gameGem))
, m_required(AZStd::move(rhs.m_required))
, m_engineDependency(AZStd::move(rhs.m_engineDependency))
{
rhs.m_id = AZ::Uuid::CreateNull();
rhs.m_version = GemVersion { 0, 0, 0 };
}
// returns whether conversion was successful
static bool LinkTypeFromString(const char* value, LinkType& linkTypeOut)
{
// static map for lookups
static const std::unordered_map<std::string, LinkType> linkNameToType = {
{ GPF_TAG_LINK_TYPE_DYNAMIC, LinkType::Dynamic },
{ GPF_TAG_LINK_TYPE_DYNAMIC_STATIC, LinkType::DynamicStatic },
{ GPF_TAG_LINK_TYPE_NO_CODE, LinkType::NoCode },
};
auto found = linkNameToType.find(value);
if (found != linkNameToType.end())
{
linkTypeOut = found->second;
return true;
}
else
{
return false;
}
}
static bool ModuleTypeFromString(const char* value, ModuleDefinition::Type& moduleTypeOut)
{
static const std::unordered_map<const char*, ModuleDefinition::Type> moduleNameToType = {
{ GPF_TAG_MODULE_TYPE_GAME_MODULE, ModuleDefinition::Type::GameModule },
{ GPF_TAG_MODULE_TYPE_SERVER_MODULE, ModuleDefinition::Type::ServerModule },
{ GPF_TAG_MODULE_TYPE_EDITOR_MODULE, ModuleDefinition::Type::EditorModule },
{ GPF_TAG_MODULE_TYPE_STATIC_LIB, ModuleDefinition::Type::StaticLib },
{ GPF_TAG_MODULE_TYPE_BUILDER, ModuleDefinition::Type::Builder },
{ GPF_TAG_MODULE_TYPE_STANDALONE, ModuleDefinition::Type::Standalone },
};
auto found = AZStd::find_if(moduleNameToType.begin(), moduleNameToType.end(), [&value](decltype(moduleNameToType)::const_reference pair) {
return strcmp(pair.first, value) == 0;
});
if (found != moduleNameToType.end())
{
moduleTypeOut = found->second;
return true;
}
else
{
return false;
}
}
// Bring contents of file up to current version.
AZ::Outcome<void, AZStd::string> UpgradeGemDescriptionJson(rapidjson::Document& descNode)
{
// get format version
if (!RAPIDJSON_IS_VALID_MEMBER(descNode, GPF_TAG_FORMAT_VERSION, IsInt))
{
return AZ::Failure(AZStd::string(GPF_TAG_FORMAT_VERSION " int is required."));
}
int gemFormatVersion = descNode[GPF_TAG_FORMAT_VERSION].GetInt();
// decline ancient and future versions
if (gemFormatVersion < 2 || gemFormatVersion > GEM_DEF_FILE_VERSION)
{
return AZ::Failure(AZStd::string::format(GPF_TAG_FORMAT_VERSION " is version %d, but %d is expected.",
gemFormatVersion, GEM_DEF_FILE_VERSION));
}
// upgrade v2 -> v3
if (gemFormatVersion < 3)
{
// beginning in v3 Gems contain an AZ::Module, in the past they contained an IGem
descNode.AddMember("IsLegacyIGem", true, descNode.GetAllocator());
}
// upgrade v3 -> v4
if (gemFormatVersion < 4)
{
// read link type, if not NoCode, migrate to GameModule
if (!RAPIDJSON_IS_VALID_MEMBER(descNode, GPF_TAG_LINK_TYPE, IsString))
{
return AZ::Failure(AZStd::string(GPF_TAG_LINK_TYPE " string must not be empty."));
}
// Explicitly copy string so we can remove it from the object
AZStd::string linkTypeString = descNode[GPF_TAG_LINK_TYPE].GetString();
descNode.RemoveMember(GPF_TAG_LINK_TYPE);
LinkType linkType;
if (!LinkTypeFromString(linkTypeString.c_str(), linkType))
{
return AZ::Failure(AZStd::string(GPF_TAG_LINK_TYPE " string is invalid."));
}
// If no-code, don't make module definitions
if (linkType != LinkType::NoCode)
{
// Create modules list
rapidjson::Value modulesList{ rapidjson::kArrayType };
// Create module definition
{
rapidjson::Value gameModule{ rapidjson::kObjectType };
gameModule.AddMember(GPF_TAG_MODULE_TYPE, GPF_TAG_MODULE_TYPE_GAME_MODULE, descNode.GetAllocator());
gameModule.AddMember(GPF_TAG_LINK_TYPE, rapidjson::Value(linkTypeString.c_str(), descNode.GetAllocator()), descNode.GetAllocator());
modulesList.PushBack(AZStd::move(gameModule), descNode.GetAllocator());
}
// Create server module definition
if (RAPIDJSON_IS_VALID_MEMBER(descNode, GPF_TAG_MODULE_TYPE_SERVER_MODULE, IsBool) && descNode[GPF_TAG_MODULE_TYPE_SERVER_MODULE].GetBool())
{
rapidjson::Value serverModule{ rapidjson::kObjectType };
serverModule.AddMember(GPF_TAG_MODULE_TYPE, GPF_TAG_MODULE_TYPE_SERVER_MODULE, descNode.GetAllocator());
serverModule.AddMember(GPF_TAG_LINK_TYPE, rapidjson::Value(linkTypeString.c_str(), descNode.GetAllocator()), descNode.GetAllocator());
serverModule.AddMember(GPF_TAG_MODULE_NAME, "Server", descNode.GetAllocator());
modulesList.PushBack(AZStd::move(serverModule), descNode.GetAllocator());
}
// Create editor module definition
if (RAPIDJSON_IS_VALID_MEMBER(descNode, GPF_TAG_EDITOR_MODULE, IsBool) && descNode[GPF_TAG_EDITOR_MODULE].GetBool())
{
rapidjson::Value editorModule{ rapidjson::kObjectType };
editorModule.AddMember(GPF_TAG_MODULE_TYPE, GPF_TAG_MODULE_TYPE_EDITOR_MODULE, descNode.GetAllocator());
editorModule.AddMember(GPF_TAG_MODULE_NAME, "Editor", descNode.GetAllocator());
editorModule.AddMember(GPF_TAG_MODULE_EXTENDS, "GameModule", descNode.GetAllocator());
modulesList.PushBack(AZStd::move(editorModule), descNode.GetAllocator());
}
descNode.RemoveMember(GPF_TAG_EDITOR_MODULE);
// Add modules list to
descNode.AddMember(GPF_TAG_MODULES, modulesList, descNode.GetAllocator());
}
}
// file is now up to date
descNode[GPF_TAG_FORMAT_VERSION] = GEM_DEF_FILE_VERSION;
return AZ::Success();
}
AZ::Outcome<GemDescription, AZStd::string> GemDescription::CreateFromJson(
rapidjson::Document& descNode,
const AZStd::string& gemFolderPath,
const AZStd::string& absoluteFilePath)
{
// gem to build
GemDescription gem;
gem.m_path = gemFolderPath;
gem.m_absolutePath = absoluteFilePath;
AzFramework::StringFunc::Path::StripFullName(gem.m_absolutePath);
AzFramework::StringFunc::RChop(gem.m_absolutePath, 1);
if (!descNode.IsObject())
{
return AZ::Failure(AZStd::string("Json root element must be an object."));
}
// upgrade contents to current version
auto upgradeOutcome = UpgradeGemDescriptionJson(descNode);
if (!upgradeOutcome.IsSuccess())
{
return AZ::Failure(upgradeOutcome.TakeError());
}
// read name
if (RAPIDJSON_IS_VALID_MEMBER(descNode, GPF_TAG_NAME, IsString))
{
gem.m_name = descNode[GPF_TAG_NAME].GetString();
}
else
{
return AZ::Failure(AZStd::string(GPF_TAG_NAME " string must not be empty."));
}
// read display name
if (RAPIDJSON_IS_VALID_MEMBER(descNode, GPF_TAG_DISPLAY_NAME, IsString))
{
gem.m_displayName = descNode[GPF_TAG_DISPLAY_NAME].GetString();
}
// read id
if (!RAPIDJSON_IS_VALID_MEMBER(descNode, GPF_TAG_UUID, IsString))
{
return AZ::Failure(AZStd::string(GPF_TAG_UUID " string is required."));
}
gem.m_id = AZ::Uuid::CreateString(descNode[GPF_TAG_UUID].GetString());
if (gem.m_id.IsNull())
{
return AZ::Failure(AZStd::string::format(GPF_TAG_UUID " string \"%s\" is invalid.", descNode[GPF_TAG_UUID].GetString()));
}
// read version
if (RAPIDJSON_IS_VALID_MEMBER(descNode, GPF_TAG_VERSION, IsString))
{
auto versionOutcome = GemVersion::ParseFromString(descNode[GPF_TAG_VERSION].GetString());
if (versionOutcome)
{
gem.m_version = versionOutcome.GetValue();
}
else
{
return AZ::Failure(AZStd::string(versionOutcome.GetError()));
}
}
else
{
return AZ::Failure(AZStd::string(GPF_TAG_VERSION " string is required."));
}
// To reduce the potential for user error, a Gem depending on the Lumberyard engine version
// supports both arrays and strings.
if (descNode.HasMember(GPF_TAG_LY_VERSION))
{
AZStd::vector<AZStd::string> versionConstraints;
// read version constraints
// Check if the version constraint is a string first.
if(RAPIDJSON_IS_VALID_MEMBER(descNode, GPF_TAG_LY_VERSION, IsString))
{
AZStd::string constraintString = descNode[GPF_TAG_LY_VERSION].GetString();
versionConstraints.push_back(constraintString);
}
// If it wasn't a string, make sure it's an array. If not, error out.
else if (!RAPIDJSON_IS_VALID_MEMBER(descNode, GPF_TAG_LY_VERSION, IsArray))
{
return AZ::Failure(AZStd::string(GPF_TAG_LY_VERSION " array is required for engine version."));
}
// If it's an empty array, ignore it.
// For ease of use editing the Gem.json files, we support empty arrays, so users
// can leave the engine version key in without providing a value.
else if (descNode[GPF_TAG_LY_VERSION].Size() > 0)
{
const auto& constraints = descNode[GPF_TAG_LY_VERSION];
const auto& end = constraints.End();
for (auto it = constraints.Begin(); it != end; ++it)
{
const auto& constraint = *it;
if (!constraint.IsString())
{
return AZ::Failure(AZStd::string(GPF_TAG_LY_VERSION " array for engine version must contain strings."));
}
versionConstraints.push_back(constraint.GetString());
}
}
// If constraints were actually provided, create the dependency.
if(versionConstraints.size() > 0)
{
EngineDependency dep;
dep.SetID(AZ::Uuid::CreateNull());
AZ::Outcome<void, AZStd::string> outcome = dep.ParseVersions(versionConstraints);
if (!outcome)
{
return AZ::Failure(AZStd::string::format(GPF_TAG_LY_VERSION " for engine version is invalid. %s", outcome.GetError().c_str()));
}
gem.m_engineDependency = AZStd::make_shared<EngineDependency>(dep);
}
}
// dependencies
if (descNode.HasMember(GPF_TAG_DEPENDENCIES))
{
if (!descNode[GPF_TAG_DEPENDENCIES].IsArray())
{
return AZ::Failure(AZStd::string(GPF_TAG_DEPENDENCIES " must be an array."));
}
// List of descriptions of Gems we depend upon
const rapidjson::Value& depsNode = descNode[GPF_TAG_DEPENDENCIES];
const auto& end = depsNode.End();
for (auto it = depsNode.Begin(); it != end; ++it)
{
const auto& depNode = *it;
if (!depNode.IsObject())
{
return AZ::Failure(AZStd::string(GPF_TAG_DEPENDENCIES " must contain objects."));
}
// read id
if (!RAPIDJSON_IS_VALID_MEMBER(depNode, GPF_TAG_UUID, IsString))
{
return AZ::Failure(AZStd::string(GPF_TAG_UUID " string is required for dependency."));
}
const char* idStr = depNode[GPF_TAG_UUID].GetString();
AZ::Uuid id(idStr);
if (id.IsNull())
{
return AZ::Failure(AZStd::string::format(GPF_TAG_UUID " in dependency is invalid: %s.", idStr));
}
// read version constraints
if (!RAPIDJSON_IS_VALID_MEMBER(depNode, GPF_TAG_VERSION_CONSTRAINTS, IsArray))
{
return AZ::Failure(AZStd::string(GPF_TAG_VERSION_CONSTRAINTS " array is required for dependency."));
}
// Make sure versions are specified
if (depNode[GPF_TAG_VERSION_CONSTRAINTS].Size() < 1)
{
return AZ::Failure(AZStd::string(GPF_TAG_VERSION_CONSTRAINTS " must have at least 1 entry for dependency."));
}
AZStd::vector<AZStd::string> versionConstraints;
const auto& constraints = depNode[GPF_TAG_VERSION_CONSTRAINTS];
const auto& constraintsEnd(constraints.End());
for (auto constraintIt = constraints.Begin(); constraintIt != constraintsEnd; ++constraintIt)
{
const auto& constraint = *constraintIt;
if (!constraint.IsString())
{
return AZ::Failure(AZStd::string(GPF_TAG_VERSION_CONSTRAINTS " array for dependency must contain strings."));
}
versionConstraints.push_back(constraint.GetString());
}
// create Dependency
GemDependency dep;
dep.SetID(id);
if (!dep.ParseVersions(versionConstraints))
{
return AZ::Failure(AZStd::string(GPF_TAG_VERSION_CONSTRAINTS " for dependency is invalid"));
}
gem.m_gemDependencies.push_back(AZStd::make_shared<GemDependency>(dep));
}
}
// Is Game Gem? flag
if (RAPIDJSON_IS_VALID_MEMBER(descNode, GPF_TAG_IS_GAME_GEM, IsBool))
{
gem.m_gameGem = descNode[GPF_TAG_IS_GAME_GEM].GetBool();
}
else
{
gem.m_gameGem = false;
}
// Is Required? flag
if (RAPIDJSON_IS_VALID_MEMBER(descNode, GPF_TAG_IS_REQUIRED, IsBool))
{
gem.m_required = descNode[GPF_TAG_IS_REQUIRED].GetBool();
}
else
{
gem.m_required = false;
}
// optional metadata
if (RAPIDJSON_IS_VALID_MEMBER(descNode, GPF_TAG_SUMMARY, IsString))
{
gem.m_summary = descNode[GPF_TAG_SUMMARY].GetString();
}
if (RAPIDJSON_IS_VALID_MEMBER(descNode, GPF_TAG_ICON_PATH, IsString))
{
gem.m_iconPath = descNode[GPF_TAG_ICON_PATH].GetString();
}
if (descNode.HasMember(GPF_TAG_TAGS))
{
const rapidjson::Value& tags = descNode[GPF_TAG_TAGS];
if (tags.IsArray())
{
const auto& end = tags.End();
for (auto it = tags.Begin(); it != end; ++it)
{
const auto& tag = *it;
gem.m_tags.push_back(tag.GetString());
}
}
else
{
return AZ::Failure(AZStd::string("Value for key " GPF_TAG_TAGS " must be an array."));
}
}
// engine module class
gem.m_engineModuleClass = RAPIDJSON_IS_VALID_MEMBER(descNode, GPF_TAG_MODULE_CLASS, IsString)
? descNode[GPF_TAG_MODULE_CLASS].GetString()
: gem.GetName() + AZStd::string("Gem");
// Cache constants
char idStr[UUID_STR_BUF_LEN];
gem.GetID().ToString(idStr, UUID_STR_BUF_LEN, false, false);
AZStd::to_lower(idStr, idStr + strlen(idStr));
// Read the modules list
if (RAPIDJSON_IS_VALID_MEMBER(descNode, GPF_TAG_MODULES, IsArray))
{
bool foundDefaultModule = false;
AZStd::unordered_map<AZStd::string, AZStd::shared_ptr<ModuleDefinition>> modulesByName;
AZStd::vector<AZStd::pair<AZStd::shared_ptr<ModuleDefinition>, AZStd::string>> dependencies;
const rapidjson::Value& modulesNode = descNode[GPF_TAG_MODULES];
for (auto moduleObjPtr = modulesNode.Begin(); moduleObjPtr != modulesNode.End(); ++moduleObjPtr)
{
const rapidjson::Value& moduleObj = *moduleObjPtr;
if (!moduleObj.IsObject())
{
return AZ::Failure(AZStd::string("Each object in " GPF_TAG_MODULES " must be an object!"));
}
auto modulePtr = AZStd::make_shared<ModuleDefinition>();
gem.m_modules.emplace_back(modulePtr);
// Get the module type
if (!RAPIDJSON_IS_VALID_MEMBER(moduleObj, GPF_TAG_MODULE_TYPE, IsString))
{
return AZ::Failure(AZStd::string("Each module requires a " GPF_TAG_MODULE_TYPE " field."));
}
const char* moduleTypeStr = moduleObj[GPF_TAG_MODULE_TYPE].GetString();
if (!ModuleTypeFromString(moduleTypeStr, modulePtr->m_type))
{
return AZ::Failure(AZStd::string::format("Module type %s is invalid!", moduleTypeStr));
}
// Get the module name (default to the type)
if (RAPIDJSON_IS_VALID_MEMBER(moduleObj, GPF_TAG_MODULE_NAME, IsString))
{
modulePtr->m_name = moduleObj[GPF_TAG_MODULE_NAME].GetString();
}
else if (modulePtr->m_type == ModuleDefinition::Type::GameModule || modulePtr->m_type == ModuleDefinition::Type::ServerModule)
{
modulePtr->m_name = moduleTypeStr;
}
else
{
return AZ::Failure(AZStd::string::format("Default \"" GPF_TAG_MODULE_NAME "\" field is only supported for modules of type \"GameModule\", not %s.", moduleTypeStr));
}
// Check for duplicate names
if (modulesByName.find(modulePtr->m_name) != modulesByName.end())
{
return AZ::Failure(AZStd::string::format("Module name \"%s\" is used more than once!", modulePtr->m_name.c_str()));
}
// If the type is GameModule, omit name from file name (maintains functionality of v3)
if (modulePtr->m_type == ModuleDefinition::Type::GameModule || modulePtr->m_type == ModuleDefinition::Type::ServerModule)
{
if (!foundDefaultModule)
{
foundDefaultModule = true;
// if the module name for 'GameModule' type is specified, such as 'Private' then it needs to be appended into the gem name
if (modulePtr->m_name != moduleTypeStr)
{
modulePtr->m_fileName = AZStd::string::format("Gem.%s.%s.%s.v%s", gem.GetName().c_str(), modulePtr->m_name.c_str(), idStr, gem.GetVersion().ToString().c_str());
}
else
{
modulePtr->m_fileName = AZStd::string::format("Gem.%s.%s.v%s", gem.GetName().c_str(), idStr, gem.GetVersion().ToString().c_str());
}
}
// If LinkType is specified, read and validate it.
if (RAPIDJSON_IS_VALID_MEMBER(moduleObj, GPF_TAG_LINK_TYPE, IsString))
{
const char* linkTypeStr = moduleObj[GPF_TAG_LINK_TYPE].GetString();
if (!LinkTypeFromString(linkTypeStr, modulePtr->m_linkType))
{
return AZ::Failure(AZStd::string::format(GPF_TAG_LINK_TYPE " specified (\"%s\") is invalid", linkTypeStr));
}
}
}
// If the module needs a file name, populate it.
if (modulePtr->m_fileName.empty() && modulePtr->m_type != ModuleDefinition::Type::StaticLib)
{
modulePtr->m_fileName = AZStd::string::format("Gem.%s.%s.%s.v%s", gem.GetName().c_str(), modulePtr->m_name.c_str(), idStr, gem.GetVersion().ToString().c_str());
}
modulesByName.emplace(modulePtr->m_name, modulePtr);
// Populate extensions
if (modulePtr->m_type != ModuleDefinition::Type::StaticLib &&
RAPIDJSON_IS_VALID_MEMBER(moduleObj, GPF_TAG_MODULE_EXTENDS, IsString))
{
dependencies.emplace_back(modulePtr, AZStd::string(moduleObj[GPF_TAG_MODULE_EXTENDS].GetString()));
}
}
// Populate dependencies
for (const auto& dependencyPair : dependencies)
{
auto dependencyIterator = modulesByName.find(dependencyPair.second);
if (dependencyIterator == modulesByName.end())
{
return AZ::Failure(AZStd::string::format("Module \"%s\" depends on \"" GPF_TAG_MODULE_EXTENDS "\" invalid module \"%s\"", dependencyPair.first->m_name.c_str(), dependencyPair.second.c_str()));
}
if (dependencyIterator->second->m_type != ModuleDefinition::Type::GameModule && dependencyIterator->second->m_type != ModuleDefinition::Type::ServerModule)
{
return AZ::Failure(AZStd::string::format("Modules may only \"" GPF_TAG_MODULE_EXTENDS "\" modules of type \"" GPF_TAG_MODULE_TYPE_GAME_MODULE "\", " GPF_TAG_MODULE_TYPE_SERVER_MODULE "\"."));
}
dependencyPair.first->m_parent = dependencyIterator->second;
dependencyIterator->second->m_children.emplace_back(dependencyPair.first);
}
// Populate modulesByType
for (const auto& modulePtr : gem.m_modules)
{
gem.m_modulesByType[modulePtr->m_type].emplace_back(modulePtr);
// If this module is a GameModule, and there is no Editor override, apply it to Editor as well.
if (modulePtr->m_type == ModuleDefinition::Type::GameModule)
{
bool foundEditorModule = false;
// Check children for editor modules
for (const auto& childWeak : modulePtr->m_children)
{
auto child = childWeak.lock();
AZ_Assert(child, "Child somehow out of scope already!");
if (child->m_type == ModuleDefinition::Type::EditorModule)
{
foundEditorModule = true;
break;
}
}
if (!foundEditorModule)
{
// If no children are for editor, add module to editor list
gem.m_modulesByType[ModuleDefinition::Type::EditorModule].emplace_back(modulePtr);
}
}
}
}
return AZ::Success(AZStd::move(gem));
}
} // namespace Gems
@@ -0,0 +1,100 @@
/*
* 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 "GemRegistry/IGemRegistry.h"
#include <AzCore/std/functional.h>
#include <AzCore/JSON/document.h>
namespace Gems
{
class GemDescription
: public IGemDescription
{
public:
GemDescription(const GemDescription& rhs);
GemDescription(GemDescription&& rhs);
~GemDescription() override = default;
// IGemDescription
const AZ::Uuid& GetID() const override { return m_id; }
const AZStd::string& GetName() const override { return m_name; }
const AZStd::string& GetDisplayName() const override { return m_displayName.empty() ? m_name : m_displayName; }
const GemVersion& GetVersion() const override { return m_version; }
const AZStd::string& GetPath() const override { return m_path; }
const AZStd::string& GetAbsolutePath() const override { return m_absolutePath; }
const AZStd::string& GetSummary() const override { return m_summary; }
const AZStd::string& GetIconPath() const override { return m_iconPath; }
const AZStd::vector<AZStd::string>& GetTags() const override { return m_tags; }
const ModuleDefinitionVector& GetModules() const override { return m_modules; }
const ModuleDefinitionVector& GetModulesOfType(ModuleDefinition::Type type) const override { return m_modulesByType.find(type)->second; }
const AZStd::string& GetEngineModuleClass() const override { return m_engineModuleClass; }
const AZStd::vector<AZStd::shared_ptr<GemDependency> >& GetGemDependencies() const override { return m_gemDependencies; }
const bool IsGameGem() const override { return m_gameGem; }
const bool IsRequired() const override { return m_required; }
const AZStd::shared_ptr<EngineDependency> GetEngineDependency() const override { return m_engineDependency; }
// ~IGemDescription
// Internal methods
/// Create GemDescription from Json.
///
/// \param[in] json Json object to parse. json may be modified during parse.
/// \param[in] gemFolderPath Relative path from engine root to Gem folder.
///
/// \returns If successful, the GemDescription.
/// If unsuccessful, an explanation why parsing failed.
static AZ::Outcome<GemDescription, AZStd::string> CreateFromJson(
rapidjson::Document& json,
const AZStd::string& gemFolderPath,
const AZStd::string& absoluteFilePath);
private:
// Outsiders may not create an empty GemDescription
GemDescription();
/// The ID of the Gem
AZ::Uuid m_id;
/// The name of the Gem
AZStd::string m_name;
/// The UI-friendly name of the Gem
AZStd::string m_displayName;
/// The version of the Gem
GemVersion m_version;
/// Relative path to Gem folder
AZStd::string m_path;
/// Absolute path to Gem folder
AZStd::string m_absolutePath;
/// Summary description of the Gem
AZStd::string m_summary;
/// Icon path of the gem
AZStd::string m_iconPath;
/// Tags to associate with the Gem
AZStd::vector<AZStd::string> m_tags;
/// List of modules produced by the Gem
ModuleDefinitionVector m_modules;
/// All modules to be loaded for a given function
AZStd::unordered_map<ModuleDefinition::Type, ModuleDefinitionVector> m_modulesByType;
/// The name of the engine module class to initialize
AZStd::string m_engineModuleClass;
/// A Gem's dependencies
AZStd::vector<AZStd::shared_ptr<GemDependency> > m_gemDependencies;
/// Flag to indicate if this is a Game GEM
bool m_gameGem;
/// Flag to indicate that this is a required GEM
bool m_required;
/// A Gem's engine dependency
AZStd::shared_ptr<EngineDependency> m_engineDependency = nullptr;
};
} // namespace Gems
@@ -0,0 +1,425 @@
/*
* 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 "ProjectSettings.h"
#include "GemRegistry.h"
#include <AzCore/std/containers/vector.h>
#include <AzCore/std/smart_ptr/make_shared.h>
#include <AzCore/IO/FileIO.h>
#include <AzCore/IO/SystemFile.h>
#include <AzCore/JSON/document.h>
#include <AzCore/JSON/error/en.h>
#include <AzFramework/IO/LocalFileIO.h>
#include <AzFramework/StringFunc/StringFunc.h>
namespace Gems
{
AZ_CLASS_ALLOCATOR_IMPL(GemRegistry, AZ::SystemAllocator, 0)
AZ::Outcome<void, AZStd::string> GemRegistry::AddSearchPath(const SearchPath& searchPathIn, bool loadGemsNow)
{
SearchPath searchPath = searchPathIn;
// Remove trailing slash if present
char lastChar = *(searchPath.m_path.end() - 1);
if (lastChar == '/' ||
lastChar == '\\')
{
AzFramework::StringFunc::RChop(searchPath.m_path, 1);
}
if (AZStd::find(m_searchPaths.begin(), m_searchPaths.end(), searchPath) == m_searchPaths.end())
{
m_searchPaths.emplace_back(searchPath);
}
if (loadGemsNow)
{
return LoadGemsFromDir(searchPath);
}
else
{
return AZ::Success();
}
}
AZ::Outcome<void, AZStd::string> GemRegistry::LoadAllGemsFromDisk()
{
AZStd::string errorString;
for (const auto& searchPath : m_searchPaths)
{
auto pathOutcome = LoadGemsFromDir(searchPath);
if (!pathOutcome.IsSuccess())
{
errorString += pathOutcome.GetError() + "\n";
}
}
if (errorString.empty())
{
return AZ::Success();
}
else
{
// Remove trailing \n
return AZ::Failure(errorString.substr(0, errorString.length() - 1));
}
}
AZ::Outcome<void, AZStd::string> GemRegistry::LoadProject(const IProjectSettings& settings, bool resetPreviousProjects)
{
if (resetPreviousProjects)
{
m_gemDescs.clear();
}
for (const auto& pair : settings.GetGems())
{
const char* absolutePath = nullptr;
// First priority goes to the project root's folder
AZStd::string testGemPath = settings.GetProjectRootPath();
if (AzFramework::StringFunc::Path::ConstructFull(settings.GetProjectRootPath().c_str(), pair.second.m_path.c_str(), testGemPath, true))
{
if (AzFramework::StringFunc::Path::Join(testGemPath.c_str(), GEM_DEF_FILE, testGemPath))
{
if (AZ::IO::SystemFile::Exists(testGemPath.c_str()))
{
absolutePath = testGemPath.c_str();
}
}
}
auto loadOutcome = LoadGemDescription(pair.second.m_path, absolutePath);
if (!loadOutcome.IsSuccess())
{
return AZ::Failure(loadOutcome.GetError());
}
}
return AZ::Success();
}
IGemDescriptionConstPtr GemRegistry::GetGemDescription(const GemSpecifier& spec) const
{
IGemDescriptionConstPtr result;
auto idIt = m_gemDescs.find(spec.m_id);
if (idIt != m_gemDescs.end())
{
auto versionIt = idIt->second.find(spec.m_version);
if (versionIt != idIt->second.end())
{
result = AZStd::static_pointer_cast<IGemDescriptionConstPtr::element_type>(versionIt->second);
}
}
return result;
}
IGemDescriptionConstPtr GemRegistry::GetLatestGem(const AZ::Uuid& uuid) const
{
IGemDescriptionConstPtr result;
auto idIt = m_gemDescs.find(uuid);
if (idIt != m_gemDescs.end())
{
GemVersion latestVersion;
GemDescriptionPtr desc;
for (const auto& pair : idIt->second)
{
if (pair.first > latestVersion)
{
latestVersion = pair.first;
desc = pair.second;
}
}
if (!latestVersion.IsZero())
{
result = AZStd::static_pointer_cast<IGemDescriptionConstPtr::element_type>(desc);
}
}
return result;
}
AZStd::vector<IGemDescriptionConstPtr> GemRegistry::GetAllGemDescriptions() const
{
AZStd::vector<IGemDescriptionConstPtr> results;
for (auto && idIt : m_gemDescs)
{
for (auto && versionIt : idIt.second)
{
results.push_back(AZStd::static_pointer_cast<IGemDescriptionConstPtr::element_type>(versionIt.second));
}
}
return results;
}
AZStd::vector<IGemDescriptionConstPtr> GemRegistry::GetAllRequiredGemDescriptions() const
{
AZStd::vector<IGemDescriptionConstPtr> results;
for (auto && idIt : m_gemDescs)
{
for (auto && versionIt : idIt.second)
{
if (versionIt.second->IsRequired())
{
results.push_back(AZStd::static_pointer_cast<IGemDescriptionConstPtr::element_type>(versionIt.second));
}
}
}
return results;
}
IGemDescriptionConstPtr GemRegistry::GetProjectGemDescription(const AZStd::string& projectName) const
{
IGemDescriptionConstPtr result;
// We know searchPaths[0] is the old engine root, so we'll just use that to avoid a search
AZStd::string gemFolderPath = projectName + "/Gem";
auto descOutcome = ParseToGemDescription(gemFolderPath, nullptr);
if (descOutcome)
{
result = AZStd::make_shared<GemDescription>(descOutcome.TakeValue());
}
return result;
}
IProjectSettings* GemRegistry::CreateProjectSettings()
{
return aznew ProjectSettings(this);
}
void GemRegistry::DestroyProjectSettings(IProjectSettings* settings)
{
delete settings;
}
AZ::Outcome<void, AZStd::string> GemRegistry::LoadGemsFromDir(const SearchPath& searchPath)
{
AZ::IO::LocalFileIO fileIo;
AZStd::string errorString;
// Handles each file and directory found
// Safe to capture all by reference because the find will run sync
AZ::IO::LocalFileIO::FindFilesCallbackType fileFinderCb;
fileFinderCb = [&](const char* fullPath) -> bool
{
if (fileIo.IsDirectory(fullPath))
{
// recurse into subdirectory
// "*" filter will match all files/directories except specials ('.', '..', etc.) with the fewest compares
fileIo.FindFiles(fullPath, "*", fileFinderCb);
}
else
{
AZStd::string fileName;
AzFramework::StringFunc::Path::GetFullFileName(fullPath, fileName);
if (0 == azstricmp(fileName.c_str(), GEM_DEF_FILE))
{
// need relative path to gem folder, so strip searchPath from front and Gem.json from back
AZStd::string gemFolderRelPath = fullPath + searchPath.m_path.length();
AzFramework::StringFunc::Path::StripFullName(gemFolderRelPath);
AzFramework::StringFunc::RChop(gemFolderRelPath, 1); // Remove trailing '/'
auto skipPathFirstSepIndex = gemFolderRelPath.find_first_not_of("/\\");
gemFolderRelPath = gemFolderRelPath.substr(skipPathFirstSepIndex);
auto loadOutcome = LoadGemDescription(gemFolderRelPath, fullPath);
if (loadOutcome.IsSuccess() == false)
{
errorString += AZStd::string::format("Fail to load Gems from path %s disk. %s\n", searchPath.m_path.c_str(), loadOutcome.GetError().c_str());
}
// We found the Gem.json file but we have to keep looking to support nested gems
}
}
return true; // keep searching
};
// Scans subdirectories
fileIo.FindFiles(searchPath.m_path.c_str(), searchPath.m_filter.c_str(), fileFinderCb);
if (errorString.empty())
{
return AZ::Success();
}
else
{
// Remove trailing \n
return AZ::Failure(errorString.substr(0, errorString.length() - 1));
}
}
AZ::Outcome<IGemDescriptionConstPtr, AZStd::string> GemRegistry::LoadGemDescription(const AZStd::string& gemFolderPath, const char* absoluteFilePath)
{
auto descOutcome = ParseToGemDescription(gemFolderPath, absoluteFilePath);
if (!descOutcome)
{
return AZ::Failure(AZStd::string::format("An error occurred while parsing %s: %s", gemFolderPath.c_str(), descOutcome.GetError().c_str()));
}
auto desc = AZStd::make_shared<GemDescription>(descOutcome.TakeValue());
// If the Gem hasn't been loaded yet, add it's id to the root map
auto idIt = m_gemDescs.find(desc->GetID());
if (idIt == m_gemDescs.end())
{
idIt = m_gemDescs.emplace(desc->GetID(), AZStd::unordered_map<GemVersion, GemDescriptionPtr>()).first;
}
// If the Gem's version doesn't exist, add it, otherwise update it
auto versionIt = idIt->second.find(desc->GetVersion());
if (versionIt == idIt->second.end())
{
idIt->second.emplace(desc->GetVersion(), AZStd::move(desc));
}
else
{
versionIt->second = desc;
}
return AZ::Success(AZStd::static_pointer_cast<IGemDescriptionConstPtr::element_type>(desc));
}
AZ::Outcome<IGemDescriptionConstPtr, AZStd::string> GemRegistry::ParseToGemDescriptionPtr(const AZStd::string& gemFolderRelPath, const char* absoluteFilePath)
{
auto descOutcome = ParseToGemDescription(gemFolderRelPath, absoluteFilePath);
if (!descOutcome)
{
return AZ::Failure(AZStd::string::format("An error occurred while parsing %s: %s", gemFolderRelPath.c_str(), descOutcome.GetError().c_str()));
}
auto desc = AZStd::make_shared<GemDescription>(descOutcome.TakeValue());
return AZ::Success(AZStd::static_pointer_cast<IGemDescriptionConstPtr::element_type>(desc));
}
AZ::Outcome<GemDescription, AZStd::string> GemRegistry::ParseToGemDescription(const AZStd::string& gemFolderPath, const char* absoluteFilePath) const
{
// do we have a pluggable, engine-compatible fileIO? For things like tools, we may not be plugged
// into a game engine, and thus, we may need to use raw file io.
AZ::IO::FileIOBase* fileReader = AZ::IO::FileIOBase::GetInstance();
// build absolute path to gem file
AZStd::string filePath;
if (absoluteFilePath)
{
filePath = absoluteFilePath;
}
else
{
for (const auto& searchPath : m_searchPaths)
{
// Append relative path to search path
AzFramework::StringFunc::Path::Join(searchPath.m_path.c_str(), gemFolderPath.c_str(), filePath);
// Append file name to file path
AzFramework::StringFunc::Path::Join(filePath.c_str(), GEM_DEF_FILE, filePath);
// note that paths are case sensitive on some systems.
if (fileReader)
{
if (fileReader->Exists(filePath.c_str()))
{
break;
}
}
else
{
if (AZ::IO::SystemFile::Exists(filePath.c_str()))
{
break;
}
}
}
}
// read json
AZStd::string fileBuf;
if (fileReader)
{
// an engine compatible file reader has been attached, so use that.
AZ::IO::HandleType fileHandle = AZ::IO::InvalidHandle;
AZ::u64 fileSize = 0;
if (!fileReader->Open(filePath.c_str(), AZ::IO::OpenMode::ModeRead | AZ::IO::OpenMode::ModeBinary, fileHandle))
{
return AZ::Failure(AZStd::string::format("Failed to read %s - file read failed.", filePath.c_str()));
}
if ((!fileReader->Size(fileHandle, fileSize)) || (fileSize == 0))
{
fileReader->Close(fileHandle);
return AZ::Failure(AZStd::string::format("Failed to read %s - file read failed.", filePath.c_str()));
}
fileBuf.resize(fileSize);
if (!fileReader->Read(fileHandle, fileBuf.data(), fileSize, true))
{
fileReader->Close(fileHandle);
return AZ::Failure(AZStd::string::format("Failed to read %s - file read failed.", filePath.c_str()));
}
fileReader->Close(fileHandle);
}
else
{
// we don't have an engine file io, use raw file IO.
AZ::IO::SystemFile rawFile;
if (!rawFile.Open(filePath.c_str(), AZ::IO::SystemFile::OpenMode::SF_OPEN_READ_ONLY))
{
return AZ::Failure(AZStd::string::format("Failed to read %s - file read failed.", filePath.c_str()));
}
fileBuf.resize(rawFile.Length());
if (fileBuf.size() == 0)
{
return AZ::Failure(AZStd::string::format("Failed to read %s - file read failed.", filePath.c_str()));
}
if (rawFile.Read(fileBuf.size(), fileBuf.data()) != fileBuf.size())
{
return AZ::Failure(AZStd::string::format("Failed to read %s - file read failed.", filePath.c_str()));
}
}
rapidjson::Document document;
document.Parse(fileBuf.data());
if (document.HasParseError())
{
const char* errorStr = rapidjson::GetParseError_En(document.GetParseError());
return AZ::Failure(AZStd::string::format("Failed to parse %s: %s", filePath.c_str(), errorStr));
}
return GemDescription::CreateFromJson(document, gemFolderPath, filePath);
}
} // namespace Gems
//////////////////////////////////////////////////////////////////////////
// DLL Exported Functions
//////////////////////////////////////////////////////////////////////////
#ifndef AZ_MONOLITHIC_BUILD // Module init functions, only required when building as a DLL.
AZ_DECLARE_MODULE_INITIALIZATION
#endif//AZ_MONOLITHIC_BUILD
extern "C" AZ_DLL_EXPORT Gems::IGemRegistry * CreateGemRegistry()
{
return aznew Gems::GemRegistry();
}
extern "C" AZ_DLL_EXPORT void DestroyGemRegistry(Gems::IGemRegistry* reg)
{
delete reg;
}
+105
View File
@@ -0,0 +1,105 @@
/*
* 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 "GemRegistry/IGemRegistry.h"
#include "GemDescription.h"
#include <AzCore/std/containers/unordered_map.h>
#include <AzCore/std/containers/vector.h>
// Constants
#define UUID_STR_BUF_LEN 64
#define GEMS_ASSETS_FOLDER "Assets"
#define GEM_DEF_FILE "gem.json"
#define GEM_DEF_FILE_VERSION 4
#define GEMS_PROJECT_FILE "gems.json"
#define GEMS_PROJECT_FILE_VERSION 2
#define PROJECT_CONFIG_FILE "project.json"
// Gem project file JSON tags
#define GPF_TAG_FORMAT_VERSION "GemFormatVersion"
#define GPF_TAG_LIST_FORMAT_VERSION "GemListFormatVersion"
#define GPF_TAG_NAME "Name"
#define GPF_TAG_DISPLAY_NAME "DisplayName"
#define GPF_TAG_GEM_ARRAY "Gems"
#define GPF_TAG_UUID "Uuid"
#define GPF_TAG_LY_VERSION "LumberyardVersion"
#define GPF_TAG_VERSION "Version"
#define GPF_TAG_DEPENDENCIES "Dependencies"
#define GPF_TAG_VERSION_CONSTRAINTS "VersionConstraints"
#define GPF_TAG_PATH "Path"
#define GPF_TAG_MODULE_CLASS "EngineModuleClass"
#define GPF_TAG_EDITOR_MODULE "EditorModule"
#define GPF_TAG_SUMMARY "Summary"
#define GPF_TAG_ICON_PATH "IconPath"
#define GPF_TAG_TAGS "Tags"
#define GPF_TAG_LINK_TYPE "LinkType"
#define GPF_TAG_LINK_TYPE_DYNAMIC "Dynamic"
#define GPF_TAG_LINK_TYPE_DYNAMIC_STATIC "DynamicStatic"
#define GPF_TAG_LINK_TYPE_NO_CODE "NoCode"
#define GPF_TAG_MODULES "Modules"
#define GPF_TAG_MODULE_NAME "Name"
#define GPF_TAG_MODULE_TYPE "Type"
#define GPF_TAG_MODULE_TYPE_GAME_MODULE "GameModule"
#define GPF_TAG_MODULE_TYPE_SERVER_MODULE "ServerModule"
#define GPF_TAG_MODULE_TYPE_EDITOR_MODULE "EditorModule"
#define GPF_TAG_MODULE_TYPE_STATIC_LIB "StaticLib"
#define GPF_TAG_MODULE_TYPE_BUILDER "Builder"
#define GPF_TAG_MODULE_TYPE_STANDALONE "Standalone"
#define GPF_TAG_MODULE_EXTENDS "Extends"
#define GPF_TAG_IS_GAME_GEM "IsGameGem"
#define GPF_TAG_IS_REQUIRED "IsRequired"
#define GPF_TAG_COMMENT "_comment"
namespace Gems
{
class GemRegistry
: public IGemRegistry
{
public:
AZ_CLASS_ALLOCATOR_DECL;
//////////////////////////////////////////////////////////////////////////
// IGemRegistry
AZ::Outcome<void, AZStd::string> AddSearchPath(const SearchPath& searchPath, bool loadGemsNow) override;
AZ::Outcome<void, AZStd::string> LoadAllGemsFromDisk() override;
AZ::Outcome<void, AZStd::string> LoadProject(const IProjectSettings& settings, bool resetPreviousProjects) override;
AZ::Outcome<IGemDescriptionConstPtr, AZStd::string> ParseToGemDescriptionPtr(const AZStd::string& gemFolderRelPath, const char* absoluteFilePath) override;
IGemDescriptionConstPtr GetGemDescription(const GemSpecifier& spec) const override;
IGemDescriptionConstPtr GetLatestGem(const AZ::Uuid& uuid) const override;
AZStd::vector<IGemDescriptionConstPtr> GetAllGemDescriptions() const override;
AZStd::vector<IGemDescriptionConstPtr> GetAllRequiredGemDescriptions() const override;
IGemDescriptionConstPtr GetProjectGemDescription(const AZStd::string& projectName) const override;
IProjectSettings* CreateProjectSettings() override;
void DestroyProjectSettings(IProjectSettings* settings) override;
~GemRegistry() override = default;
//////////////////////////////////////////////////////////////////////////
private:
using GemDescriptionPtr = AZStd::shared_ptr<GemDescription>;
AZ::Outcome<void, AZStd::string> LoadGemsFromDir(const SearchPath& searchPath);
// Pass nullptr for absoluteFolderPath to do a search for the Gem
AZ::Outcome<IGemDescriptionConstPtr, AZStd::string> LoadGemDescription(const AZStd::string& gemFolderPath, const char* absoluteFilePath);
AZ::Outcome<GemDescription, AZStd::string> ParseToGemDescription(const AZStd::string& gemFolderPath, const char* absoluteFilePath) const;
AZStd::vector<SearchPath> m_searchPaths; // Explictly ordered so that AddSearchPath() order matters
AZStd::unordered_map<AZ::Uuid, AZStd::unordered_map<GemVersion, GemDescriptionPtr> > m_gemDescs;
};
} // namespace Gems
extern "C" AZ_DLL_EXPORT Gems::IGemRegistry * CreateGemRegistry();
extern "C" AZ_DLL_EXPORT void DestroyGemRegistry(Gems::IGemRegistry* reg);
@@ -0,0 +1,596 @@
/*
* 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 "ProjectSettings.h"
#include "GemRegistry.h"
#include <fstream>
#include <AzCore/std/containers/vector.h>
#include <AzCore/std/string/string.h>
#include <AzCore/std/sort.h>
#include <AzCore/std/smart_ptr/make_shared.h>
#include <AzCore/Debug/Trace.h>
#include <AzCore/IO/FileIO.h>
#include <AzCore/IO/Path/Path.h>
#include <AzCore/IO/SystemFile.h>
#include <AzFramework/FileFunc/FileFunc.h>
#include <AzCore/PlatformIncl.h>
#include <AzCore/JSON/stringbuffer.h>
#include <AzCore/JSON/prettywriter.h>
#include <AzCore/JSON/error/en.h>
#include <AzFramework/API/ApplicationAPI.h>
#if defined(AZ_PLATFORM_ANDROID)
#include <errno.h>
#endif
#define MAX_ERROR_STRING_SIZE 512
namespace Gems
{
AZ_CLASS_ALLOCATOR_IMPL(ProjectSettings, AZ::SystemAllocator, 0)
ProjectSettings::ProjectSettings(GemRegistry* registry)
: m_registry(registry)
, m_initialized(false)
{
}
AZ::Outcome<void, AZStd::string> ProjectSettings::Initialize(const AZStd::string& appRootFolder, const AZStd::string& projectSubFolder)
{
AZ_Assert(!m_initialized, "ProjectSettings has been initialized already.");
// Initialize the app root folder
m_projectRootPath = appRootFolder;
// Project gems file lives in (ProjectFolder)/gems.json - which might be @assets@/gems.json or an absolute path (in tools)
m_gemsSettingsFilePath = appRootFolder;
AzFramework::StringFunc::Path::Join(m_gemsSettingsFilePath.c_str(), projectSubFolder.c_str(), m_gemsSettingsFilePath);
AzFramework::StringFunc::Path::Join(m_gemsSettingsFilePath.c_str(), GEMS_PROJECT_FILE, m_gemsSettingsFilePath);
// Project config file lives in (ProjectFolder)/project.json - which might be @assets@/project.json or an absolute path (in tools)
m_projectSettingsFilePath = appRootFolder;
AzFramework::StringFunc::Path::Join(m_projectSettingsFilePath.c_str(), projectSubFolder.c_str(), m_projectSettingsFilePath);
AzFramework::StringFunc::Path::Join(m_projectSettingsFilePath.c_str(), PROJECT_CONFIG_FILE, m_projectSettingsFilePath);
auto loadOutcome = LoadSettings();
m_initialized = loadOutcome.IsSuccess();
return loadOutcome;
}
bool ProjectSettings::EnableGem(const ProjectGemSpecifier& spec)
{
auto it = m_gems.find(spec.m_id);
if (it != m_gems.end())
{
// If the Gem is already enabled, update the version and path of the entry.
it->second.m_version = spec.m_version;
it->second.m_path = spec.m_path;
}
else
{
// create entry based on data from registry
m_gems.insert(AZStd::make_pair(spec.m_id, spec));
}
return true;
}
bool ProjectSettings::DisableGem(const GemSpecifier& spec)
{
auto it = m_gems.find(spec.m_id);
// If the Gem is enabled at the version specified, disable it.
if (it != m_gems.end())
{
if (spec.m_version != it->second.m_version)
{
return false;
}
m_gems.erase(it);
}
return true;
}
bool ProjectSettings::IsGemEnabled(const GemSpecifier& spec) const
{
auto it = m_gems.find(spec.m_id);
return it != m_gems.end()
&& it->second.m_version == spec.m_version;
}
bool ProjectSettings::IsGemEnabled(const AZ::Uuid& id, const AZStd::vector<AZStd::string>& versionConstraints) const
{
AZStd::shared_ptr<GemDependency> dependency = AZStd::make_shared<GemDependency>();
dependency->SetID(id);
auto parseOutcome = dependency->ParseVersions(versionConstraints);
if (!parseOutcome.IsSuccess())
{
AZ_Assert(false, parseOutcome.GetError().c_str());
return false;
}
return IsGemDependencyMet(dependency);
}
bool ProjectSettings::IsGemDependencyMet(const AZStd::shared_ptr<GemDependency> dep) const
{
// Gems can depend on other Gems
auto it = m_gems.find(dep->GetID());
return it != m_gems.end()
&& dep->IsFullfilledBy(it->second);
}
bool ProjectSettings::IsEngineDependencyMet(const AZStd::shared_ptr<EngineDependency> dep, const EngineVersion& againstVersion) const
{
EngineSpecifier engineSpecifier(AZ::Uuid::CreateNull(), againstVersion);
return dep->IsFullfilledBy(engineSpecifier);
}
class GemDependencyInfo : public GemDependency
{
public:
GemDependencyInfo(IGemDescriptionConstPtr gem)
: GemDependency()
, m_gem{gem}
{
}
IGemDescriptionConstPtr GetGem() const
{
return m_gem;
}
private:
IGemDescriptionConstPtr m_gem;
};
AZ::Outcome<void, AZStd::string> ProjectSettings::ValidateDependencies(const EngineVersion& engineVersion) const
{
AZStd::unordered_map<AZ::Uuid, GemDependencyInfo> globalDeps;
// Build list of required Gems
for (const auto& pair : m_gems)
{
const ProjectGemSpecifier& spec = pair.second;
auto gem = m_registry->GetGemDescription(spec);
if (!gem)
{
return AZ::Failure(AZStd::string::format("Gem with Id \"%s\" not found.", pair.first.ToString<AZStd::string>().c_str()));
}
for (auto && gemDep : gem->GetGemDependencies())
{
const AZ::Uuid id = gemDep->GetID();
GemDependency* dep;
// If the dependency isn't tracked globally, create a new one
auto depIt = globalDeps.find(id);
if (depIt == globalDeps.end())
{
globalDeps.insert(AZStd::make_pair(id, GemDependencyInfo(gem)));
dep = &globalDeps.at(id);
dep->m_id = id;
}
else
{
dep = &depIt->second;
}
// These bounds should be normalized before verification to make sure there aren't conflicting bounds
dep->m_bounds.insert(dep->m_bounds.end(), gemDep->GetBounds().begin(), gemDep->GetBounds().end());
}
}
AZStd::string errorString;
bool isTreeValid = true;
// Verify all engine dependencies are met
for(const auto& pair : m_gems)
{
const ProjectGemSpecifier& spec = pair.second;
auto gem = m_registry->GetGemDescription(spec);
if (!gem)
{
errorString += AZStd::string::format("Gem with Id \"%s\" not found.", pair.first.ToString<AZStd::string>().c_str());
isTreeValid = false;
continue;
}
// do not verify the engine version if input is default constructed
if (engineVersion == EngineVersion())
{
continue;
}
// Check the Gem's engine dependency
auto engineDepPtr = gem->GetEngineDependency();
if (engineDepPtr && !IsEngineDependencyMet(engineDepPtr, engineVersion))
{
AZStd::string errmsg = AZStd::string::format("Gem with Id \"%s\" does not meet the Lumberyard engine version requirement.\n",
pair.first.ToString<AZStd::string>().c_str());
// do not force an assertion to happen here, we are just printing the warning and letting the user
// decide on how to handle it if the engine start up fails.
errorString += errmsg;
AZ_Warning("GemRegistry", false, errmsg.c_str());
}
}
// attempt to construct a complete gem registry for unmet dependency ID to name resolution
GemRegistry completeRegistry;
const char* gemsSearchFilter = "Gems";
const char* appRoot = nullptr;
AzFramework::ApplicationRequests::Bus::BroadcastResult(appRoot, &AzFramework::ApplicationRequests::GetAppRoot);
if (appRoot)
{
completeRegistry.AddSearchPath({ appRoot, gemsSearchFilter }, false);
}
const char* engineRoot = nullptr;
AzFramework::ApplicationRequests::Bus::BroadcastResult(engineRoot, &AzFramework::ApplicationRequests::GetEngineRoot);
if (engineRoot)
{
completeRegistry.AddSearchPath({ engineRoot, gemsSearchFilter }, false);
}
completeRegistry.LoadAllGemsFromDisk();
// Verify all gems dependencies are all met
for (auto && pair : globalDeps)
{
const GemDependencyInfo& dep = pair.second;
// Find candidate in project's listed gems
auto candidateIt = m_gems.find(dep.GetID());
if (candidateIt == m_gems.end())
{
// no candidate found
char depIdStr[UUID_STR_BUF_LEN];
dep.GetID().ToString(depIdStr, UUID_STR_BUF_LEN, true, true);
char gemIdStr[UUID_STR_BUF_LEN];
dep.GetGem()->GetID().ToString(gemIdStr, UUID_STR_BUF_LEN, true, true);
// don't care about the version, just need the gem name
IGemDescriptionConstPtr depDesc = completeRegistry.GetLatestGem(dep.GetID());
if (depDesc)
{
errorString += AZStd::string::format(
"Gem \"%s\" (%s) dependency on Gem \"%s\" (%s) is unmet.\n",
dep.GetGem()->GetDisplayName().c_str(),
gemIdStr,
depDesc->GetDisplayName().c_str(),
depIdStr
);
}
else
{
errorString += AZStd::string::format(
"Gem \"%s\" (%s) dependency on unresolved Gem with ID %s is unmet.\n",
dep.GetGem()->GetDisplayName().c_str(),
gemIdStr,
depIdStr
);
}
isTreeValid = false;
}
else if (!dep.IsFullfilledBy(candidateIt->second))
{
// candidate found, but it doesn't fulfill all dependency requirements
AZStd::string boundsStr;
for (auto && bound : dep.m_bounds)
{
if (boundsStr.length() == 0)
{
boundsStr = bound.ToString();
}
else
{
boundsStr += ", " + bound.ToString();
}
}
char depIdStr[UUID_STR_BUF_LEN];
dep.GetID().ToString(depIdStr, UUID_STR_BUF_LEN, true, true);
char gemIdStr[UUID_STR_BUF_LEN];
dep.GetGem()->GetID().ToString(gemIdStr, UUID_STR_BUF_LEN, true, true);
// don't care about the version, just need the gem name
IGemDescriptionConstPtr depDesc = completeRegistry.GetLatestGem(dep.GetID());
if (depDesc)
{
errorString += AZStd::string::format(
"Gem \"%s\" (%s) dependency on Gem \"%s\" (%s) is unmet. It must fall within the following version bounds: [%s]\n",
dep.GetGem()->GetDisplayName().c_str(),
gemIdStr,
depDesc->GetDisplayName().c_str(),
depIdStr,
boundsStr.c_str()
);
}
else
{
errorString += AZStd::string::format(
"Gem \"%s\" (%s) dependency on unresolved Gem with ID %s is unmet. It must fall within the following version bounds: [%s]\n",
dep.GetGem()->GetDisplayName().c_str(),
gemIdStr,
depIdStr,
boundsStr.c_str()
);
}
isTreeValid = false;
}
}
if (!isTreeValid)
{
return AZ::Failure(errorString);
}
return AZ::Success();
}
AZ::Outcome<void, AZStd::string> ProjectSettings::Save() const
{
using namespace AZ::IO;
FileIOBase* fileIo = FileIOBase::GetInstance();
HandleType projectSettingsHandle = InvalidHandle;
if (fileIo->Open(m_gemsSettingsFilePath.c_str(), OpenMode::ModeWrite | OpenMode::ModeText, projectSettingsHandle))
{
rapidjson::Document jsonRep = GetJsonRepresentation();
rapidjson::StringBuffer buffer;
rapidjson::PrettyWriter<rapidjson::StringBuffer> writer(buffer);
jsonRep.Accept(writer);
AZ::u64 bytesWritten = 0;
if (!fileIo->Write(projectSettingsHandle, buffer.GetString(), buffer.GetSize(), &bytesWritten))
{
return AZ::Failure(AZStd::string::format("Failed to write Gems settings to file: %s", m_gemsSettingsFilePath.c_str()));
}
if (bytesWritten != buffer.GetSize())
{
return AZ::Failure(AZStd::string::format("Failed to write complete Gems settings to file: %s", m_gemsSettingsFilePath.c_str()));
}
fileIo->Close(projectSettingsHandle);
return AZ::Success();
}
else
{
char errorBuffer[MAX_ERROR_STRING_SIZE];
#if defined(AZ_PLATFORM_WINDOWS)
FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM,
nullptr,
GetLastError(),
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
errorBuffer,
MAX_ERROR_STRING_SIZE,
nullptr);
#else
azstrerror_s(errorBuffer, MAX_ERROR_STRING_SIZE, errno);
#endif // defined(AZ_PLATFORM_WINDOWS)
return AZ::Failure(AZStd::string::format("Failed to open %s for write: %s", m_gemsSettingsFilePath.c_str(), errorBuffer));
}
}
const AZStd::string& ProjectSettings::GetProjectName() const
{
return m_projectName;
}
const AZStd::string& ProjectSettings::GetProjectRootPath() const
{
return m_projectRootPath;
}
AZ::Outcome<void, AZStd::string> ProjectSettings::LoadSettings()
{
// an engine compatible file reader has been attached, so use that.
AZ::IO::FileIOBase* fileReader = AZ::IO::FileIOBase::GetInstance();
// Read and parse the gems.json file
{
AZ::IO::Path gemsSettingsPath(m_gemsSettingsFilePath);
auto readGemsJsonResult = AzFramework::FileFunc::ReadJsonFile(gemsSettingsPath, fileReader);
if (!readGemsJsonResult.IsSuccess())
{
return AZ::Failure(AZStd::string::format("Failed to read Json file %s: %s",
m_gemsSettingsFilePath.c_str(), readGemsJsonResult.GetError().c_str()));
}
auto parseGemsJsonResult = ParseGemsJson(readGemsJsonResult.GetValue());
if (!parseGemsJsonResult.IsSuccess())
{
return AZ::Failure(AZStd::string::format("Failed to parse Json file %s: %s",
m_gemsSettingsFilePath.c_str(), parseGemsJsonResult.GetError().c_str()));
}
}
// Read and parse the project.json file
{
AZ::IO::Path projectSettingsPath(m_projectSettingsFilePath);
auto readProjectJsonResult = AzFramework::FileFunc::ReadJsonFile(projectSettingsPath, fileReader);
if (!readProjectJsonResult.IsSuccess())
{
return AZ::Failure(AZStd::string::format("Failed to read Json file %s: %s",
m_gemsSettingsFilePath.c_str(), readProjectJsonResult.GetError().c_str()));
}
auto parseProjectJsonResult = ParseProjectJson(readProjectJsonResult.GetValue());
if (!parseProjectJsonResult.IsSuccess())
{
return AZ::Failure(AZStd::string::format("Failed to parse Json file %s: %s",
m_gemsSettingsFilePath.c_str(), parseProjectJsonResult.GetError().c_str()));
}
}
return AZ::Success();
}
rapidjson::Document ProjectSettings::GetJsonRepresentation() const
{
rapidjson::Document rootObj(rapidjson::kObjectType);
rootObj.AddMember<int>(GPF_TAG_LIST_FORMAT_VERSION, GEMS_PROJECT_FILE_VERSION, rootObj.GetAllocator());
// We want to write out Gems in the same order each time.
// Create vector for sorting.
AZStd::vector<const ProjectGemSpecifier*> sortedGems(m_gems.size());
auto transformFn = [](const ProjectGemSpecifierMap::value_type& pair) { return &pair.second; };
AZStd::transform(m_gems.begin(), m_gems.end(), sortedGems.begin(), transformFn);
// we'll sort based on ID.
AZStd::sort(sortedGems.begin(), sortedGems.end(), [](const ProjectGemSpecifier* a, const ProjectGemSpecifier* b) -> bool
{
return a->m_id < b->m_id;
});
auto addMember = [&rootObj](rapidjson::Value& obj, const char* key, const char* str)
{
rapidjson::Value k(rapidjson::StringRef(key), rootObj.GetAllocator());
rapidjson::Value v(rapidjson::StringRef(str), rootObj.GetAllocator());
obj.AddMember(k.Move(), v.Move(), rootObj.GetAllocator());
};
// build Gems array
rapidjson::Value gemsArray(rapidjson::kArrayType);
for (const ProjectGemSpecifier* gemSpec : sortedGems)
{
char idStr[UUID_STR_BUF_LEN];
gemSpec->m_id.ToString(idStr, UUID_STR_BUF_LEN, false, false);
AZStd::to_lower(idStr, idStr + strlen(idStr));
AZStd::string path = gemSpec->m_path;
// Replace '\' with '/'
AZStd::replace(path.begin(), path.end(), '\\', '/');
// Remove trailing slash
if (*path.rbegin() == '/')
{
path.pop_back();
}
rapidjson::Value gemObj(rapidjson::kObjectType);
addMember(gemObj, GPF_TAG_PATH, path.c_str());
addMember(gemObj, GPF_TAG_UUID, idStr);
addMember(gemObj, GPF_TAG_VERSION, gemSpec->m_version.ToString().c_str());
// write name in comment (if possible)
if (IGemDescriptionConstPtr gemDesc = m_registry->GetGemDescription(*gemSpec))
{
addMember(gemObj, GPF_TAG_COMMENT, gemDesc->GetName().c_str());
}
gemsArray.PushBack(gemObj, rootObj.GetAllocator());
}
rootObj.AddMember(GPF_TAG_GEM_ARRAY, gemsArray, rootObj.GetAllocator());
return rootObj;
}
AZ::Outcome<void, AZStd::string> ProjectSettings::ParseGemsJson(const rapidjson::Document& jsonRep)
{
// check version
if (!RAPIDJSON_IS_VALID_MEMBER(jsonRep, GPF_TAG_LIST_FORMAT_VERSION, IsInt))
{
return AZ::Failure(AZStd::string(GPF_TAG_LIST_FORMAT_VERSION " number is required."));
}
int gemListFormatVersion = jsonRep[GPF_TAG_LIST_FORMAT_VERSION].GetInt();
if (gemListFormatVersion != GEMS_PROJECT_FILE_VERSION)
{
return AZ::Failure(AZStd::string::format(
GPF_TAG_LIST_FORMAT_VERSION " is version %d, but %d is expected.",
gemListFormatVersion,
GEMS_PROJECT_FILE_VERSION));
}
// read gems
if (!RAPIDJSON_IS_VALID_MEMBER(jsonRep, GPF_TAG_GEM_ARRAY, IsArray))
{
return AZ::Failure(AZStd::string(GPF_TAG_GEM_ARRAY " list is required"));
}
const rapidjson::Value& gemList = jsonRep[GPF_TAG_GEM_ARRAY];
const auto& end = gemList.End();
for (auto it = gemList.Begin(); it != end; ++it)
{
const auto& elem = *it;
// gem id
if (!RAPIDJSON_IS_VALID_MEMBER(elem, GPF_TAG_UUID, IsString))
{
return AZ::Failure(AZStd::string(GPF_TAG_UUID " string is required for Gem."));
}
const char* idStr = elem[GPF_TAG_UUID].GetString();
AZ::Uuid id = AZ::Uuid::CreateString(idStr);
if (id.IsNull())
{
return AZ::Failure(AZStd::string(GPF_TAG_UUID " string is invalid for Gem."));
}
// gem version
if (!RAPIDJSON_IS_VALID_MEMBER(elem, GPF_TAG_VERSION, IsString))
{
return AZ::Failure(AZStd::string::format(
GPF_TAG_VERSION " string is missing for Gem with ID %s.",
idStr));
}
auto versionOutcome = GemVersion::ParseFromString(elem[GPF_TAG_VERSION].GetString());
if (!versionOutcome)
{
return AZ::Failure(AZStd::string::format(
GPF_TAG_VERSION " string is invalid for Gem with ID %s: %s",
idStr, versionOutcome.GetError().c_str()));
}
GemVersion version = versionOutcome.GetValue();
// gem path
if (!RAPIDJSON_IS_VALID_MEMBER(elem, GPF_TAG_PATH, IsString))
{
return AZ::Failure(AZStd::string(GPF_TAG_PATH " string is required for Gem"));
}
const char* path = elem[GPF_TAG_PATH].GetString();
EnableGem(ProjectGemSpecifier(id, version, path));
}
return AZ::Success();
}
AZ::Outcome<void, AZStd::string> ProjectSettings::ParseProjectJson(const rapidjson::Document& json)
{
// For now, we only
static const char* project_name_key = "project_name";
if (!RAPIDJSON_IS_VALID_MEMBER(json, project_name_key, IsString))
{
return AZ::Failure(AZStd::string::format("Missing/Invalid key '%s' in project.json.", project_name_key));
}
m_projectName = json[project_name_key].GetString();
return AZ::Success();
}
} // namespace Gems
@@ -0,0 +1,69 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#include <AzCore/std/containers/unordered_map.h>
#include <AzCore/JSON/document.h>
#include "GemRegistry.h"
namespace Gems
{
class ProjectSettings
: public IProjectSettings
{
public:
AZ_CLASS_ALLOCATOR_DECL
ProjectSettings(GemRegistry* registry);
~ProjectSettings() override = default;
// IProjectSettings
AZ::Outcome<void, AZStd::string> Initialize(const AZStd::string& appRootFolder, const AZStd::string& projectSubFolder) override;
bool EnableGem(const ProjectGemSpecifier& spec) override;
bool DisableGem(const GemSpecifier& spec) override;
bool IsGemEnabled(const GemSpecifier& spec) const override;
bool IsGemEnabled(const AZ::Uuid& id, const AZStd::vector<AZStd::string>& versionConstraints) const override;
bool IsGemDependencyMet(const AZStd::shared_ptr<GemDependency> dep) const override;
bool IsEngineDependencyMet(const AZStd::shared_ptr<EngineDependency> dep, const EngineVersion& againstVersion) const override;
const ProjectGemSpecifierMap& GetGems() const override { return m_gems; }
void SetGems(const ProjectGemSpecifierMap& newGemMap) override { m_gems = newGemMap; }
AZ::Outcome<void, AZStd::string> ValidateDependencies(const EngineVersion& engineVersion) const override;
AZ::Outcome<void, AZStd::string> Save() const override;
const AZStd::string& GetProjectName() const override;
const AZStd::string& GetProjectRootPath() const override;
// ~IProjectSettings
// Internal methods
/// Loads settings from the path provided by m_settingsFilePath
AZ::Outcome<void, AZStd::string> LoadSettings();
/// Converts the ProjectGemSpecifierMap (m_gems) into it's Json representation for saving
rapidjson::Document GetJsonRepresentation() const;
/// Converts GEMS Json into the ProjectGemSpecifierMap (m_gems)
AZ::Outcome<void, AZStd::string> ParseGemsJson(const rapidjson::Document& json);
/// Reads from project.json to initialize project-specific values
AZ::Outcome<void, AZStd::string> ParseProjectJson(const rapidjson::Document& json);
private:
ProjectGemSpecifierMap m_gems;
GemRegistry* m_registry;
AZStd::string m_gemsSettingsFilePath;
AZStd::string m_projectSettingsFilePath;
AZStd::string m_projectName;
AZStd::string m_projectRootPath;
bool m_initialized;
};
} // namespace Gems