You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
o3de/Gems/ScriptCanvas/Code/Include/ScriptCanvas/Internal/Nodes/ExpressionNodeBase.cpp

487 lines
19 KiB
C++

/*
* Copyright (c) Contributors to the Open 3D Engine Project. For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include "ExpressionNodeBase.h"
#include <AzCore/std/algorithm.h>
#include <AzCore/Component/TickBus.h>
#include <AzCore/Serialization/Utils.h>
#include <ScriptCanvas/Core/Graph.h>
#include <ScriptCanvas/Debugger/ValidationEvents/DataValidation/InvalidExpressionEvent.h>
namespace ScriptCanvas
{
namespace Nodes
{
namespace Internal
{
///////////////////////
// ExpressionNodeBase
///////////////////////
ExpressionNodeBase::ExpressionNodeBase()
: m_isInError(false)
{
}
AZ::Outcome<DependencyReport, void> ExpressionNodeBase::GetDependencies() const
{
return AZ::Success(DependencyReport{});
}
AZStd::string ExpressionNodeBase::GetRawFormat() const
{
return m_format;
}
const AZStd::unordered_map<AZStd::string, SlotId>& ExpressionNodeBase::GetSlotsByName() const
{
return m_slotsByVariables;
}
void ExpressionNodeBase::OnInit()
{
m_stringInterface.SetPropertyReference(&m_format);
m_stringInterface.RegisterListener(this);
const auto& variableList = m_expressionTree.GetVariables();
for (const auto& variableName : variableList)
{
Slot* variableSlot = GetSlotByName(variableName);
if (variableSlot)
{
m_slotToVariableMap[variableSlot->GetId()] = variableName;
m_slotsByVariables[variableName] = variableSlot->GetId();
}
}
}
void ExpressionNodeBase::OnPostActivate()
{
for (auto variableListing : m_slotToVariableMap)
{
Slot* variableSlot = GetSlot(variableListing.first);
if (variableSlot && !HasConnectedNodes((*variableSlot)))
{
const Datum* datum = FindDatum(variableSlot->GetId());
if (datum)
{
PushVariable(variableListing.second, (*datum));
}
}
}
}
void ExpressionNodeBase::ConfigureVisualExtensions()
{
{
VisualExtensionSlotConfiguration visualExtensions(VisualExtensionSlotConfiguration::VisualExtensionType::ExtenderSlot);
visualExtensions.m_name = "Add Input";
visualExtensions.m_tooltip = "Adds an input to the current expression format";
visualExtensions.m_displayGroup = GetDisplayGroup();
visualExtensions.m_connectionType = ConnectionType::Input;
visualExtensions.m_identifier = GetExtensionId();
RegisterExtension(visualExtensions);
}
{
VisualExtensionSlotConfiguration visualExtensions(VisualExtensionSlotConfiguration::VisualExtensionType::PropertySlot);
visualExtensions.m_name = "";
visualExtensions.m_tooltip = "";
visualExtensions.m_displayGroup = GetDisplayGroup();
visualExtensions.m_connectionType = ConnectionType::Input;
visualExtensions.m_identifier = GetPropertyId();
RegisterExtension(visualExtensions);
}
}
bool ExpressionNodeBase::CanDeleteSlot([[maybe_unused]] const SlotId& slotId) const
{
return m_handlingExtension;
}
SlotId ExpressionNodeBase::HandleExtension(AZ::Crc32 extensionId)
{
if (extensionId == GetExtensionId() && !m_isInError)
{
int value = 0;
AZStd::string name = "Value";
Slot* slot = GetSlotByName(name);
while (slot)
{
++value;
name = AZStd::string::format("Value_%i", value);
slot = GetSlotByName(name);
}
if (!m_format.empty())
{
m_format.append(GetExpressionSeparator());
}
m_format.append("{");
m_format.append(name);
m_format.append("}");
m_stringInterface.SignalDataChanged();
slot = GetSlotByName(name);
if (slot)
{
m_handlingExtension = true;
return slot->GetId();
}
}
return SlotId();
}
void ExpressionNodeBase::ExtensionCancelled([[maybe_unused]] AZ::Crc32 extensionId)
{
// Remove the seperator operator if we added it.
if (!m_format.empty())
{
AZStd::string seperator = GetExpressionSeparator();
m_format = m_format.substr(0, m_format.length() - seperator.size());
m_stringInterface.SignalDataChanged();
}
m_handlingExtension = false;
}
void ExpressionNodeBase::FinalizeExtension([[maybe_unused]] AZ::Crc32 extensionId)
{
m_handlingExtension = false;
}
NodePropertyInterface* ExpressionNodeBase::GetPropertyInterface(AZ::Crc32 propertyId)
{
if (propertyId == GetPropertyId())
{
return &m_stringInterface;
}
return nullptr;
}
void ExpressionNodeBase::OnSlotRemoved(const SlotId& slotId)
{
if (m_parsingFormat)
{
return;
}
auto mapIter = m_slotToVariableMap.find(slotId);
if (mapIter != m_slotToVariableMap.end())
{
AZStd::string name = AZStd::string::format("{%s}", mapIter->second.c_str());
AZStd::size_t firstInstance = m_format.find(name);
if (firstInstance == AZStd::string::npos)
{
return;
}
m_format.erase(firstInstance, name.size());
if (!m_handlingExtension)
{
m_stringInterface.SignalDataChanged();
}
}
}
bool ExpressionNodeBase::OnValidateNode(ValidationResults& validationResults)
{
// This means we were loaded up with an error, but we haven't re-parsed so we
// don't know what the error is. Force a reparse here to ensure
// we report errors correctly.
if (m_isInError && m_parseError.IsValidExpression())
{
const bool signalError = false;
ParseFormat(signalError);
}
if (!m_parseError.IsValidExpression())
{
InvalidExpressionEvent* invaildExpressionEvent = aznew InvalidExpressionEvent(GetEntityId(), m_parseError.m_errorString);
validationResults.AddValidationEvent(invaildExpressionEvent);
return false;
}
return true;
}
void ExpressionNodeBase::ParseFormat(bool signalError)
{
ExpressionEvaluation::ParseOutcome parseOutcome = ParseExpression(m_format);
if (!parseOutcome.IsSuccess())
{
m_isInError = true;
m_parseError = parseOutcome.GetError();
if (signalError)
{
AZStd::string parseError = m_parseError.m_errorString;
GetGraph()->ReportError((*this), "Parsing Error", parseError);
}
return;
}
else
{
m_isInError = false;
m_parseError.Clear();
m_parsingFormat = true;
AZStd::unordered_map< AZStd::string, SlotCacheSetup > variableSlotMapping;
m_expressionTree = parseOutcome.GetValue();
const AZStd::vector< AZStd::string >& variableList = m_expressionTree.GetVariables();
for (const AZStd::string& variableString : variableList)
{
Slot* slot = GetSlotByName(variableString);
if (slot)
{
if (slot->IsExecution() || slot->IsOutput())
{
m_isInError = true;
m_parseError = ExpressionEvaluation::ParsingError();
m_parseError.m_offsetIndex = m_format.find_first_of(variableString);
const auto& totalSlots = GetSlots();
AZStd::string reservedNames;
for (const auto& slot2 : totalSlots)
{
if (slot2.IsExecution() || slot2.IsOutput())
{
if (!reservedNames.empty())
{
reservedNames.append(", ");
}
reservedNames.append(slot2.GetName());
}
}
m_parseError.m_errorString = AZStd::string::format("Using one of the reserved slot names \"%s\" in expression at position %zu", reservedNames.c_str(), m_parseError.m_offsetIndex);
AZStd::string parseError = m_parseError.m_errorString;
GetGraph()->ReportError((*this), "Parsing Error", parseError);
return;
}
SlotCacheSetup& cacheSetup = variableSlotMapping[variableString];
cacheSetup.m_previousId = slot->GetId();
cacheSetup.m_displayType = slot->GetDisplayType();
cacheSetup.m_reference = slot->GetVariableReference();
const Datum* datum = FindDatum(slot->GetId());
if (datum)
{
cacheSetup.m_defaultValue.ReconfigureDatumTo((*datum));
}
}
}
AZStd::vector<const Slot*> eraseSlots = GetAllSlotsByDescriptor(SlotDescriptors::DataIn());
for (const Slot* eraseSlot : eraseSlots)
{
// If we have the name in our mapping. We want to keep its connections since we're going to
// recreate it.
size_t newMapping = variableSlotMapping.count(eraseSlot->GetName());
bool signalRemoval = newMapping == 0;
SlotId slotId = eraseSlot->GetId();
RemoveSlot(slotId, signalRemoval);
if (signalRemoval)
{
m_slotToVariableMap.erase(slotId);
}
}
// Start our counting from our raw variable position ignoring any other slots that might have been added.
int slotOrder = static_cast<int>(GetSlots().size()) -1;
for (const auto& variableName : variableList)
{
const AZStd::vector<AZ::Uuid>& supportedTypes = m_expressionTree.GetSupportedTypes(variableName);
if (supportedTypes.empty())
{
// Just bypass any slots that have no valid types for now. We can sort it out later.
continue;
}
auto mappingIter = variableSlotMapping.find(variableName);
bool isNewSlot = (mappingIter == variableSlotMapping.end());
SlotId slotId;
// If we have a single data type. Create a typed data slot.
if (supportedTypes.size() == 1)
{
ScriptCanvas::Data::Type dataType = Data::FromAZType(supportedTypes.front());
if (dataType.IsValid())
{
DataSlotConfiguration dataSlotConfiguration(dataType);
ConfigureSlot(variableName, dataSlotConfiguration);
if (mappingIter != variableSlotMapping.end())
{
dataSlotConfiguration.m_slotId = mappingIter->second.m_previousId;
if (dataType != mappingIter->second.m_displayType && mappingIter->second.m_displayType.IsValid())
{
AZ_Error("ScriptCanvas", false, "Variable supported type changed. Need to invalidate all connections. Currently unsupported.");
}
dataSlotConfiguration.ConfigureDatum(AZStd::move(mappingIter->second.m_defaultValue));
}
slotId = InsertSlot(slotOrder, dataSlotConfiguration, isNewSlot);
}
}
// Otherwise if we can support multiple types make a dynamic slot.
else
{
DynamicDataSlotConfiguration dynamicSlotConfiguration;
ConfigureSlot(variableName, dynamicSlotConfiguration);
dynamicSlotConfiguration.m_dynamicDataType = DynamicDataType::Any;
AZStd::vector< ScriptCanvas::Data::Type > dataTypes;
dataTypes.reserve(supportedTypes.size());
for (const auto& supportedType : supportedTypes)
{
dataTypes.emplace_back(ScriptCanvas::Data::FromAZType(supportedType));
}
dynamicSlotConfiguration.m_contractDescs = AZStd::vector<ScriptCanvas::ContractDescriptor>
{
// Restricted Type Contract
{ [dataTypes]() { return aznew ScriptCanvas::RestrictedTypeContract(dataTypes); } }
};
if (mappingIter != variableSlotMapping.end())
{
dynamicSlotConfiguration.m_slotId = mappingIter->second.m_previousId;
dynamicSlotConfiguration.m_displayType = mappingIter->second.m_displayType;
}
slotId = InsertSlot(slotOrder, dynamicSlotConfiguration, isNewSlot);
}
Slot* slot = GetSlot(slotId);
if (slot && mappingIter != variableSlotMapping.end())
{
if (mappingIter->second.m_reference.IsValid())
{
slot->SetVariableReference(mappingIter->second.m_reference);
}
}
m_slotToVariableMap[slotId] = variableName;
++slotOrder;
}
m_parsingFormat = false;
}
}
void ExpressionNodeBase::SignalFormatChanged()
{
// Adding and removing slots within the ChangeNotify handler causes problems due to the property grid's
// rows changing. To get around that problem we queue the parsing of the string format to happen
// on the next system tick.
AZ::SystemTickBus::QueueFunction([this]() {
m_stringInterface.SignalDataChanged();
});
}
ExpressionEvaluation::ParseOutcome ExpressionNodeBase::ParseExpression([[maybe_unused]] const AZStd::string& formatString)
{
AZ_Assert(false , "Implementing node must override OnResult.");
ExpressionEvaluation::ParsingError error;
error.m_errorString = "Unable to parse string due to unknown parsing parameters";
return AZ::Failure(error);
}
AZStd::string ExpressionNodeBase::GetExpressionSeparator() const
{
return "";
}
void ExpressionNodeBase::PushVariable(const AZStd::string& variableName, const Datum& datum)
{
auto& variableValue = m_expressionTree.ModVariable(variableName);
variableValue = datum.ToAny();
}
void ExpressionNodeBase::OnPropertyChanged()
{
ParseFormat();
}
void ExpressionNodeBase::ConfigureSlot(const AZStd::string& variableName, SlotConfiguration& slotConfiguration)
{
AZStd::string tooltip = AZStd::string::format("Value which replaces instances of {%s} in the resulting expression.", variableName.c_str());
slotConfiguration.m_name = variableName;
slotConfiguration.m_toolTip = tooltip;
slotConfiguration.m_displayGroup = GetDisplayGroup();
slotConfiguration.SetConnectionType(ConnectionType::Input);
slotConfiguration.m_addUniqueSlotByNameAndType = true;
}
}
}
}