diff --git a/Core/GDCore/Events/CodeGeneration/EventsCodeGenerator.cpp b/Core/GDCore/Events/CodeGeneration/EventsCodeGenerator.cpp index d63c676f76..805f75c048 100644 --- a/Core/GDCore/Events/CodeGeneration/EventsCodeGenerator.cpp +++ b/Core/GDCore/Events/CodeGeneration/EventsCodeGenerator.cpp @@ -1212,13 +1212,13 @@ gd::String EventsCodeGenerator::GeneratePropertyGetter(const gd::PropertiesConta const gd::NamedPropertyDescriptor& property, const gd::String& type, gd::EventsCodeGenerationContext& context) { - return "getProperty" + property.GetName() + "()"; + return "getProperty" + property.GetName() + "As" + type + "()"; } gd::String EventsCodeGenerator::GenerateParameterGetter(const gd::ParameterMetadata& parameter, const gd::String& type, gd::EventsCodeGenerationContext& context) { - return "getParameter" + parameter.GetName() + "()"; + return "getParameter" + parameter.GetName() + "As" + type + "()"; } EventsCodeGenerator::EventsCodeGenerator(const gd::Project& project_, diff --git a/Core/GDCore/Events/CodeGeneration/EventsCodeGenerator.h b/Core/GDCore/Events/CodeGeneration/EventsCodeGenerator.h index 4767814013..6e6c8b2298 100644 --- a/Core/GDCore/Events/CodeGeneration/EventsCodeGenerator.h +++ b/Core/GDCore/Events/CodeGeneration/EventsCodeGenerator.h @@ -12,8 +12,8 @@ #include "GDCore/Events/Event.h" #include "GDCore/Events/Instruction.h" -#include "GDCore/String.h" #include "GDCore/Project/ProjectScopedContainers.h" +#include "GDCore/String.h" namespace gd { class EventsList; class Expression; @@ -58,8 +58,9 @@ class GD_CORE_API EventsCodeGenerator { * \brief Construct a code generator for the specified * objects/groups and platform */ - EventsCodeGenerator(const gd::Platform& platform, - const gd::ProjectScopedContainers& projectScopedContainers_); + EventsCodeGenerator( + const gd::Platform& platform, + const gd::ProjectScopedContainers& projectScopedContainers_); virtual ~EventsCodeGenerator(){}; /** @@ -546,7 +547,9 @@ class GD_CORE_API EventsCodeGenerator { }; virtual gd::String GenerateVariableValueAs(const gd::String& type) { - return type == "string" ? ".getAsString()" : ".getAsNumber()"; + return type == "number|string" ? ".getAsNumberOrString()" + : type == "string" ? ".getAsString()" + : ".getAsNumber()"; } /** @@ -577,14 +580,16 @@ class GD_CORE_API EventsCodeGenerator { return "fakeObjectListOf_" + objectName; } - virtual gd::String GeneratePropertyGetter(const gd::PropertiesContainer& propertiesContainer, - const gd::NamedPropertyDescriptor& property, - const gd::String& type, - gd::EventsCodeGenerationContext& context); + virtual gd::String GeneratePropertyGetter( + const gd::PropertiesContainer& propertiesContainer, + const gd::NamedPropertyDescriptor& property, + const gd::String& type, + gd::EventsCodeGenerationContext& context); - virtual gd::String GenerateParameterGetter(const gd::ParameterMetadata& parameter, - const gd::String& type, - gd::EventsCodeGenerationContext& context); + virtual gd::String GenerateParameterGetter( + const gd::ParameterMetadata& parameter, + const gd::String& type, + gd::EventsCodeGenerationContext& context); /** * \brief Generate the code to reference an object which is @@ -665,7 +670,8 @@ class GD_CORE_API EventsCodeGenerator { * The default implementation generates C-style code : It wraps the predicate * inside parenthesis and add a !. */ - virtual gd::String GenerateNegatedPredicate(const gd::String& predicate) const { + virtual gd::String GenerateNegatedPredicate( + const gd::String& predicate) const { return "!(" + predicate + ")"; }; diff --git a/Core/GDCore/Events/CodeGeneration/ExpressionCodeGenerator.cpp b/Core/GDCore/Events/CodeGeneration/ExpressionCodeGenerator.cpp index 49b7ad2772..d3f81ac6f5 100644 --- a/Core/GDCore/Events/CodeGeneration/ExpressionCodeGenerator.cpp +++ b/Core/GDCore/Events/CodeGeneration/ExpressionCodeGenerator.cpp @@ -103,7 +103,7 @@ void ExpressionCodeGenerator::OnVisitVariableNode(VariableNode& node) { // This "translation" from the type to an enum could be avoided // if all types were moved to an enum. auto type = gd::ExpressionTypeFinder::GetType(codeGenerator.GetPlatform(), - codeGenerator.GetObjectsContainersList(), + codeGenerator.GetProjectScopedContainers(), rootType, node); @@ -191,7 +191,7 @@ void ExpressionCodeGenerator::OnVisitVariableBracketAccessorNode( return; } - ExpressionCodeGenerator generator("string", "", codeGenerator, context); + ExpressionCodeGenerator generator("number|string", "", codeGenerator, context); node.expression->Visit(generator); output += codeGenerator.GenerateVariableBracketAccessor(generator.GetOutput()); @@ -200,7 +200,7 @@ void ExpressionCodeGenerator::OnVisitVariableBracketAccessorNode( void ExpressionCodeGenerator::OnVisitIdentifierNode(IdentifierNode& node) { auto type = gd::ExpressionTypeFinder::GetType(codeGenerator.GetPlatform(), - codeGenerator.GetObjectsContainersList(), + codeGenerator.GetProjectScopedContainers(), rootType, node); @@ -271,7 +271,7 @@ void ExpressionCodeGenerator::OnVisitIdentifierNode(IdentifierNode& node) { void ExpressionCodeGenerator::OnVisitFunctionCallNode(FunctionCallNode& node) { auto type = gd::ExpressionTypeFinder::GetType(codeGenerator.GetPlatform(), - codeGenerator.GetObjectsContainersList(), + codeGenerator.GetProjectScopedContainers(), rootType, node); @@ -502,7 +502,7 @@ gd::String ExpressionCodeGenerator::GenerateDefaultValue( void ExpressionCodeGenerator::OnVisitEmptyNode(EmptyNode& node) { auto type = gd::ExpressionTypeFinder::GetType(codeGenerator.GetPlatform(), - codeGenerator.GetObjectsContainersList(), + codeGenerator.GetProjectScopedContainers(), rootType, node); output += GenerateDefaultValue(type); @@ -511,7 +511,7 @@ void ExpressionCodeGenerator::OnVisitEmptyNode(EmptyNode& node) { void ExpressionCodeGenerator::OnVisitObjectFunctionNameNode( ObjectFunctionNameNode& node) { auto type = gd::ExpressionTypeFinder::GetType(codeGenerator.GetPlatform(), - codeGenerator.GetObjectsContainersList(), + codeGenerator.GetProjectScopedContainers(), rootType, node); output += GenerateDefaultValue(type); diff --git a/Core/GDCore/IDE/Events/EventsContextAnalyzer.cpp b/Core/GDCore/IDE/Events/EventsContextAnalyzer.cpp index 63b5c444ea..58aac2059c 100644 --- a/Core/GDCore/IDE/Events/EventsContextAnalyzer.cpp +++ b/Core/GDCore/IDE/Events/EventsContextAnalyzer.cpp @@ -58,7 +58,7 @@ class GD_CORE_API ExpressionObjectsAnalyzer void OnVisitNumberNode(NumberNode& node) override {} void OnVisitTextNode(TextNode& node) override {} void OnVisitVariableNode(VariableNode& node) override { - auto type = gd::ExpressionTypeFinder::GetType(platform, projectScopedContainers.GetObjectsContainersList(), rootType, node); + auto type = gd::ExpressionTypeFinder::GetType(platform, projectScopedContainers, rootType, node); if (gd::ParameterMetadata::IsExpression("variable", type)) { // Nothing to do (this can't reference an object) @@ -88,7 +88,7 @@ class GD_CORE_API ExpressionObjectsAnalyzer if (node.child) node.child->Visit(*this); } void OnVisitIdentifierNode(IdentifierNode& node) override { - auto type = gd::ExpressionTypeFinder::GetType(platform, projectScopedContainers.GetObjectsContainersList(), rootType, node); + auto type = gd::ExpressionTypeFinder::GetType(platform, projectScopedContainers, rootType, node); if (gd::ParameterMetadata::IsObject(type)) { context.AddObjectName(projectScopedContainers, node.identifierName); } else if (gd::ParameterMetadata::IsExpression("variable", type)) { diff --git a/Core/GDCore/IDE/Events/EventsRefactorer.cpp b/Core/GDCore/IDE/Events/EventsRefactorer.cpp index 500d0af55e..e7370bfd2f 100644 --- a/Core/GDCore/IDE/Events/EventsRefactorer.cpp +++ b/Core/GDCore/IDE/Events/EventsRefactorer.cpp @@ -81,8 +81,7 @@ class GD_CORE_API ExpressionObjectRenamer : public ExpressionParser2NodeWorker { void OnVisitNumberNode(NumberNode& node) override {} void OnVisitTextNode(TextNode& node) override {} void OnVisitVariableNode(VariableNode& node) override { - const auto& objectsContainersList = projectScopedContainers.GetObjectsContainersList(); - auto type = gd::ExpressionTypeFinder::GetType(platform, objectsContainersList, rootType, node); + auto type = gd::ExpressionTypeFinder::GetType(platform, projectScopedContainers, rootType, node); if (gd::ValueTypeMetadata::IsTypeLegacyPreScopedVariable(type)) { // Nothing to do (this can't reference an object) @@ -115,7 +114,7 @@ class GD_CORE_API ExpressionObjectRenamer : public ExpressionParser2NodeWorker { if (node.child) node.child->Visit(*this); } void OnVisitIdentifierNode(IdentifierNode& node) override { - auto type = gd::ExpressionTypeFinder::GetType(platform, projectScopedContainers.GetObjectsContainersList(), rootType, node); + auto type = gd::ExpressionTypeFinder::GetType(platform, projectScopedContainers, rootType, node); if (gd::ParameterMetadata::IsObject(type) && node.identifierName == objectName) { hasDoneRenaming = true; @@ -217,8 +216,7 @@ class GD_CORE_API ExpressionObjectFinder : public ExpressionParser2NodeWorker { void OnVisitNumberNode(NumberNode& node) override {} void OnVisitTextNode(TextNode& node) override {} void OnVisitVariableNode(VariableNode& node) override { - const auto& objectsContainersList = projectScopedContainers.GetObjectsContainersList(); - auto type = gd::ExpressionTypeFinder::GetType(platform, objectsContainersList, rootType, node); + auto type = gd::ExpressionTypeFinder::GetType(platform, projectScopedContainers, rootType, node); if (gd::ValueTypeMetadata::IsTypeLegacyPreScopedVariable(type)) { // Nothing to do (this can't reference an object) @@ -250,7 +248,7 @@ class GD_CORE_API ExpressionObjectFinder : public ExpressionParser2NodeWorker { if (node.child) node.child->Visit(*this); } void OnVisitIdentifierNode(IdentifierNode& node) override { - auto type = gd::ExpressionTypeFinder::GetType(platform, projectScopedContainers.GetObjectsContainersList(), rootType, node); + auto type = gd::ExpressionTypeFinder::GetType(platform, projectScopedContainers, rootType, node); if (gd::ParameterMetadata::IsObject(type) && node.identifierName == searchedObjectName) { hasObject = true; diff --git a/Core/GDCore/IDE/Events/ExpressionCompletionFinder.h b/Core/GDCore/IDE/Events/ExpressionCompletionFinder.h index 8b252119df..17c5c35873 100644 --- a/Core/GDCore/IDE/Events/ExpressionCompletionFinder.h +++ b/Core/GDCore/IDE/Events/ExpressionCompletionFinder.h @@ -395,10 +395,8 @@ class GD_CORE_API ExpressionCompletionFinder protected: void OnVisitSubExpressionNode(SubExpressionNode& node) override { - const auto& objectsContainersList = - projectScopedContainers.GetObjectsContainersList(); auto type = gd::ExpressionTypeFinder::GetType( - platform, objectsContainersList, rootType, node); + platform, projectScopedContainers, rootType, node); AddCompletionsForAllIdentifiersWithPrefix("", type); completions.push_back( @@ -409,10 +407,8 @@ class GD_CORE_API ExpressionCompletionFinder // No completions. } void OnVisitUnaryOperatorNode(UnaryOperatorNode& node) override { - const auto& objectsContainersList = - projectScopedContainers.GetObjectsContainersList(); auto type = gd::ExpressionTypeFinder::GetType( - platform, objectsContainersList, rootType, node); + platform, projectScopedContainers, rootType, node); AddCompletionsForAllIdentifiersWithPrefix("", type); completions.push_back( @@ -488,7 +484,7 @@ class GD_CORE_API ExpressionCompletionFinder const auto& objectsContainersList = projectScopedContainers.GetObjectsContainersList(); auto type = gd::ExpressionTypeFinder::GetType( - platform, objectsContainersList, rootType, node); + platform, projectScopedContainers, rootType, node); if (gd::ValueTypeMetadata::IsTypeLegacyPreScopedVariable(type)) { if (type == "globalvar") { @@ -535,7 +531,7 @@ class GD_CORE_API ExpressionCompletionFinder const auto& objectsContainersList = projectScopedContainers.GetObjectsContainersList(); auto type = gd::ExpressionTypeFinder::GetType( - platform, objectsContainersList, rootType, node); + platform, projectScopedContainers, rootType, node); if (gd::ParameterMetadata::IsObject(type)) { // Only show completions of objects if an object is required. AddCompletionsForObjectWithPrefix( @@ -615,10 +611,8 @@ class GD_CORE_API ExpressionCompletionFinder } } void OnVisitObjectFunctionNameNode(ObjectFunctionNameNode& node) override { - const auto& objectsContainersList = - projectScopedContainers.GetObjectsContainersList(); auto type = gd::ExpressionTypeFinder::GetType( - platform, objectsContainersList, rootType, node); + platform, projectScopedContainers, rootType, node); if (!node.behaviorFunctionName.empty() || node.behaviorNameNamespaceSeparatorLocation.IsValid()) { // Behavior function (or behavior function being written, with the @@ -669,10 +663,8 @@ class GD_CORE_API ExpressionCompletionFinder } } void OnVisitFunctionCallNode(FunctionCallNode& node) override { - const auto& objectsContainersList = - projectScopedContainers.GetObjectsContainersList(); auto type = gd::ExpressionTypeFinder::GetType( - platform, objectsContainersList, rootType, node); + platform, projectScopedContainers, rootType, node); bool isCaretOnParenthesis = IsCaretOn(node.openingParenthesisLocation) || IsCaretOn(node.closingParenthesisLocation); @@ -741,10 +733,8 @@ class GD_CORE_API ExpressionCompletionFinder } } void OnVisitEmptyNode(EmptyNode& node) override { - const auto& objectsContainersList = - projectScopedContainers.GetObjectsContainersList(); auto type = gd::ExpressionTypeFinder::GetType( - platform, objectsContainersList, rootType, node); + platform, projectScopedContainers, rootType, node); AddCompletionsForAllIdentifiersWithPrefix(node.text, type, node.location); completions.push_back( diff --git a/Core/GDCore/IDE/Events/ExpressionLeftSideTypeFinder.h b/Core/GDCore/IDE/Events/ExpressionLeftSideTypeFinder.h index 56765f5aa0..2b1cc6879e 100644 --- a/Core/GDCore/IDE/Events/ExpressionLeftSideTypeFinder.h +++ b/Core/GDCore/IDE/Events/ExpressionLeftSideTypeFinder.h @@ -14,13 +14,12 @@ #include "GDCore/Extensions/Metadata/MetadataProvider.h" #include "GDCore/Extensions/Metadata/ObjectMetadata.h" #include "GDCore/Extensions/Metadata/ParameterMetadata.h" -#include "GDCore/Project/Layout.h" // For GetTypeOfObject and GetTypeOfBehavior +#include "GDCore/Project/ProjectScopedContainers.h" #include "GDCore/Tools/Localization.h" namespace gd { class Expression; class ObjectsContainer; -class ObjectsContainersList; class Platform; class ParameterMetadata; class ExpressionMetadata; @@ -41,10 +40,10 @@ class GD_CORE_API ExpressionLeftSideTypeFinder : public ExpressionParser2NodeWor * operations. */ static const gd::String GetType(const gd::Platform &platform, - const gd::ObjectsContainersList &objectsContainersList, + const gd::ProjectScopedContainers &projectScopedContainers, gd::ExpressionNode& node) { gd::ExpressionLeftSideTypeFinder typeFinder( - platform, objectsContainersList); + platform, projectScopedContainers); node.Visit(typeFinder); return typeFinder.GetType(); } @@ -53,9 +52,9 @@ class GD_CORE_API ExpressionLeftSideTypeFinder : public ExpressionParser2NodeWor protected: ExpressionLeftSideTypeFinder(const gd::Platform &platform_, - const gd::ObjectsContainersList &objectsContainersList_) + const gd::ProjectScopedContainers &projectScopedContainers_) : platform(platform_), - objectsContainersList(objectsContainersList_), + projectScopedContainers(projectScopedContainers_), type("unknown") {}; const gd::String &GetType() { @@ -67,6 +66,14 @@ class GD_CORE_API ExpressionLeftSideTypeFinder : public ExpressionParser2NodeWor } void OnVisitOperatorNode(OperatorNode& node) override { node.leftHandSide->Visit(*this); + + // The type is decided by the first operand, unless it can (`number|string`) + // or should (`unknown`) be refined, in which case we go for the right + // operand (which got visited knowing the type of the first operand, so it's + // equal or strictly more precise than the left operand). + if (type == "unknown" || type == "number|string") { + node.rightHandSide->Visit(*this); + } } void OnVisitUnaryOperatorNode(UnaryOperatorNode& node) override { node.factor->Visit(*this); @@ -83,7 +90,7 @@ class GD_CORE_API ExpressionLeftSideTypeFinder : public ExpressionParser2NodeWor } void OnVisitFunctionCallNode(FunctionCallNode& node) override { const gd::ExpressionMetadata &metadata = MetadataProvider::GetFunctionCallMetadata( - platform, objectsContainersList, node); + platform, projectScopedContainers.GetObjectsContainersList(), node); if (gd::MetadataProvider::IsBadExpressionMetadata(metadata)) { type = "unknown"; } @@ -93,12 +100,99 @@ class GD_CORE_API ExpressionLeftSideTypeFinder : public ExpressionParser2NodeWor } void OnVisitVariableNode(VariableNode& node) override { type = "unknown"; + + projectScopedContainers.MatchIdentifierWithName(node.name, + [&]() { + // This represents an object. + // We could store it to explore the type of the variable, but in practice this + // is only called for structures/arrays with 2 levels, and we don't support structure + // type identification for now. + }, + [&]() { + // This is a variable. + // We could store it to explore the type of the variable, but in practice this + // is only called for structures/arrays with 2 levels, and we don't support structure + // type identification for now. + }, [&]() { + // This is a property with more than one child - this is unsupported. + }, [&]() { + // This is a parameter with more than one child - this is unsupported. + }, [&]() { + // This is something else. + type = "unknown"; + }); } void OnVisitVariableAccessorNode(VariableAccessorNode& node) override { type = "unknown"; } void OnVisitIdentifierNode(IdentifierNode& node) override { type = "unknown"; + projectScopedContainers.MatchIdentifierWithName(node.identifierName, + [&]() { + // It's an object variable. + if (projectScopedContainers.GetObjectsContainersList() + .HasObjectOrGroupWithVariableNamed( + node.identifierName, node.childIdentifierName) + == ObjectsContainersList::VariableExistence::DoesNotExist) { + type = "unknown"; + return; + } + + auto variableType = + projectScopedContainers.GetObjectsContainersList() + .GetTypeOfObjectOrGroupVariable(node.identifierName, + node.childIdentifierName); + ReadTypeFromVariable(variableType); + }, + [&]() { + // It's a variable. + const auto& variable = + projectScopedContainers.GetVariablesContainersList().Get( + node.identifierName); + + if (node.childIdentifierName.empty()) { + ReadTypeFromVariable(variable.GetType()); + } else { + if (!variable.HasChild(node.childIdentifierName)) { + type = "unknown"; + return; + } + + ReadTypeFromVariable( + variable.GetChild(node.childIdentifierName).GetType()); + } + }, [&]() { + // This is a property. + const gd::NamedPropertyDescriptor& property = projectScopedContainers + .GetPropertiesContainersList().Get(node.identifierName).second; + + if (property.GetType() == "Number") { + type = "number"; + } else if (property.GetType() == "Boolean") { + // Nothing - we don't know the precise type (this could be used a string or as a number) + } else { + // Assume type is String or equivalent. + type = "string"; + } + }, [&]() { + // It's a parameter. + + const auto& parametersVectorsList = projectScopedContainers.GetParametersVectorsList(); + const auto& parameter = gd::ParameterMetadataTools::Get(parametersVectorsList, node.identifierName); + const auto& valueTypeMetadata = parameter.GetValueTypeMetadata(); + if (valueTypeMetadata.IsNumber()) { + type = "number"; + } else if (valueTypeMetadata.IsString()) { + type = "string"; + } else if (valueTypeMetadata.IsBoolean()) { + // Nothing - we don't know the precise type (this could be used as a string or as a number). + } else { + type = "unknown"; + } + }, [&]() { + // This is something else. + type = "unknown"; + }); } void OnVisitEmptyNode(EmptyNode& node) override { type = "unknown"; @@ -108,10 +202,18 @@ class GD_CORE_API ExpressionLeftSideTypeFinder : public ExpressionParser2NodeWor } private: + void ReadTypeFromVariable(gd::Variable::Type variableType) { + if (variableType == gd::Variable::Number) { + type = "number"; + } else if (variableType == gd::Variable::String) { + type = "string"; + } + } + gd::String type; const gd::Platform &platform; - const gd::ObjectsContainersList &objectsContainersList; + const gd::ProjectScopedContainers &projectScopedContainers; const gd::String rootType; }; diff --git a/Core/GDCore/IDE/Events/ExpressionTypeFinder.h b/Core/GDCore/IDE/Events/ExpressionTypeFinder.h index 9e9924972f..4a03157e37 100644 --- a/Core/GDCore/IDE/Events/ExpressionTypeFinder.h +++ b/Core/GDCore/IDE/Events/ExpressionTypeFinder.h @@ -15,13 +15,12 @@ #include "GDCore/Extensions/Metadata/MetadataProvider.h" #include "GDCore/Extensions/Metadata/ObjectMetadata.h" #include "GDCore/Extensions/Metadata/ParameterMetadata.h" -#include "GDCore/Project/Layout.h" // For GetTypeOfObject and GetTypeOfBehavior +#include "GDCore/Project/ProjectScopedContainers.h" #include "GDCore/Tools/Localization.h" namespace gd { class Expression; class ObjectsContainer; -class ObjectsContainersList; class Platform; class ParameterMetadata; class ExpressionMetadata; @@ -51,11 +50,11 @@ class GD_CORE_API ExpressionTypeFinder : public ExpressionParser2NodeWorker { * sub-expression that a given node represents. */ static const gd::String GetType(const gd::Platform &platform, - const gd::ObjectsContainersList &objectsContainersList, + const gd::ProjectScopedContainers &projectScopedContainers, const gd::String &rootType, gd::ExpressionNode& node) { gd::ExpressionTypeFinder typeFinder( - platform, objectsContainersList, rootType); + platform, projectScopedContainers, rootType); node.Visit(typeFinder); return typeFinder.GetType(); } @@ -64,10 +63,10 @@ class GD_CORE_API ExpressionTypeFinder : public ExpressionParser2NodeWorker { protected: ExpressionTypeFinder(const gd::Platform &platform_, - const gd::ObjectsContainersList &objectsContainersList_, + const gd::ProjectScopedContainers &projectScopedContainers_, const gd::String &rootType_) : platform(platform_), - objectsContainersList(objectsContainersList_), + projectScopedContainers(projectScopedContainers_), rootType(rootType_), type(ExpressionTypeFinder::unknownType), child(nullptr) {}; @@ -113,8 +112,12 @@ class GD_CORE_API ExpressionTypeFinder : public ExpressionParser2NodeWorker { } auto leftSideType = gd::ExpressionLeftSideTypeFinder::GetType( platform, - objectsContainersList, + projectScopedContainers, node); + + // If we can infer a definitive number or string type, use it. + // Otherwise, we only know that it's number or string, and this can even + // be used as is at runtime. if (leftSideType == ExpressionTypeFinder::numberType || leftSideType == ExpressionTypeFinder::stringType) { type = leftSideType; @@ -126,7 +129,7 @@ class GD_CORE_API ExpressionTypeFinder : public ExpressionParser2NodeWorker { void OnVisitFunctionCallNode(FunctionCallNode& node) override { if (child == nullptr) { const gd::ExpressionMetadata &metadata = MetadataProvider::GetFunctionCallMetadata( - platform, objectsContainersList, node); + platform, projectScopedContainers.GetObjectsContainersList(), node); if (gd::MetadataProvider::IsBadExpressionMetadata(metadata)) { VisitParent(node); } @@ -138,7 +141,7 @@ class GD_CORE_API ExpressionTypeFinder : public ExpressionParser2NodeWorker { const gd::ParameterMetadata* parameterMetadata = gd::MetadataProvider::GetFunctionCallParameterMetadata( platform, - objectsContainersList, + projectScopedContainers.GetObjectsContainersList(), node, *child); if (parameterMetadata == nullptr || parameterMetadata->GetType().empty()) { @@ -159,7 +162,7 @@ class GD_CORE_API ExpressionTypeFinder : public ExpressionParser2NodeWorker { else if (rootType == ExpressionTypeFinder::numberOrStringType) { auto leftSideType = gd::ExpressionLeftSideTypeFinder::GetType( platform, - objectsContainersList, + projectScopedContainers, node); if (leftSideType == ExpressionTypeFinder::numberType || leftSideType == ExpressionTypeFinder::stringType) { @@ -183,7 +186,7 @@ class GD_CORE_API ExpressionTypeFinder : public ExpressionParser2NodeWorker { ExpressionNode *child; const gd::Platform &platform; - const gd::ObjectsContainersList &objectsContainersList; + const gd::ProjectScopedContainers &projectScopedContainers; const gd::String rootType; }; diff --git a/Core/GDCore/IDE/Events/ExpressionValidator.cpp b/Core/GDCore/IDE/Events/ExpressionValidator.cpp index 4b207c329d..6202ea0a36 100644 --- a/Core/GDCore/IDE/Events/ExpressionValidator.cpp +++ b/Core/GDCore/IDE/Events/ExpressionValidator.cpp @@ -89,12 +89,17 @@ bool ExpressionValidator::ValidateObjectVariableOrVariableOrProperty( const auto& propertiesContainersList = projectScopedContainers.GetPropertiesContainersList(); const auto& parametersVectorsList = projectScopedContainers.GetParametersVectorsList(); + // Unless we find something precise (like a variable or property or parameter with a known type), + // we consider this node will be of the type required by the parent. + childType = parentType; + return projectScopedContainers.MatchIdentifierWithName(identifier.identifierName, [&]() { // This represents an object. if (identifier.childIdentifierName.empty()) { RaiseTypeError(_("An object variable or expression should be entered."), identifier.identifierNameLocation); + return true; // We should have found a variable. } @@ -103,26 +108,31 @@ bool ExpressionValidator::ValidateObjectVariableOrVariableOrProperty( if (variableExistence == gd::ObjectsContainersList::DoesNotExist) { RaiseTypeError(_("This variable does not exist on this object or group."), identifier.childIdentifierNameLocation); + return true; // We should have found a variable. } else if (variableExistence == gd::ObjectsContainersList::ExistsOnlyOnSomeObjectsOfTheGroup) { RaiseTypeError(_("This variable only exists on some objects of the group. It must be declared for all objects."), identifier.childIdentifierNameLocation); + return true; // We should have found a variable. } else if (variableExistence == gd::ObjectsContainersList::GroupIsEmpty) { RaiseTypeError(_("This group is empty. Add an object to this group first."), identifier.identifierNameLocation); + return true; // We should have found a variable. } + auto variableType = objectsContainersList.GetTypeOfObjectOrGroupVariable(identifier.identifierName, identifier.childIdentifierName); + ReadChildTypeFromVariable(variableType); + return true; // We found a variable. }, [&]() { // This is a variable. // Try to identify a declared variable with the name (and maybe the child // variable). - const gd::Variable& variable = variablesContainersList.Get(identifier.identifierName); @@ -130,6 +140,7 @@ bool ExpressionValidator::ValidateObjectVariableOrVariableOrProperty( // Just the root variable is accessed, check it can be used in an // expression. validateVariableTypeForExpression(variable.GetType()); + ReadChildTypeFromVariable(variable.GetType()); return true; // We found a variable. } else { @@ -143,6 +154,7 @@ bool ExpressionValidator::ValidateObjectVariableOrVariableOrProperty( const gd::Variable& childVariable = variable.GetChild(identifier.childIdentifierName); + ReadChildTypeFromVariable(childVariable.GetType()); return true; // We found a variable. } }, [&]() { @@ -150,23 +162,44 @@ bool ExpressionValidator::ValidateObjectVariableOrVariableOrProperty( if (!identifier.childIdentifierName.empty()) { RaiseTypeError(_("Accessing a child variable of a property is not possible - just write the property name."), identifier.childIdentifierNameLocation); + return true; // We found a property, even if the child is not allowed. } + const gd::NamedPropertyDescriptor& property = projectScopedContainers + .GetPropertiesContainersList().Get(identifier.identifierName).second; + + if (property.GetType() == "Number") { + childType = Type::Number; + } else if (property.GetType() == "Boolean") { + // Nothing - we don't know the precise type (this could be used a string or as a number) + } else { + // Assume type is String or equivalent. + childType = Type::String; + } + return true; // We found a property. }, [&]() { // This is a parameter. if (!identifier.childIdentifierName.empty()) { RaiseTypeError(_("Accessing a child variable of a parameter is not possible - just write the parameter name."), identifier.childIdentifierNameLocation); + return true; // We found a parameter, even if the child is not allowed. } const auto& parameter = gd::ParameterMetadataTools::Get(parametersVectorsList, identifier.identifierName); const auto& valueTypeMetadata = parameter.GetValueTypeMetadata(); - if (!valueTypeMetadata.IsNumber() && !valueTypeMetadata.IsString() && !valueTypeMetadata.IsBoolean()) { + if (valueTypeMetadata.IsNumber()) { + childType = Type::Number; + } else if (valueTypeMetadata.IsString()) { + childType = Type::String; + } else if (valueTypeMetadata.IsBoolean()) { + // Nothing - we don't know the precise type (this could be used as a string or as a number). + } else { RaiseTypeError(_("This parameter is not a string, number or boolean - it can't be used in an expression."), identifier.identifierNameLocation); + return true; // We found a parameter, even though the type is incompatible. } diff --git a/Core/GDCore/IDE/Events/ExpressionValidator.h b/Core/GDCore/IDE/Events/ExpressionValidator.h index b0341915a3..b1170e3000 100644 --- a/Core/GDCore/IDE/Events/ExpressionValidator.h +++ b/Core/GDCore/IDE/Events/ExpressionValidator.h @@ -103,9 +103,9 @@ class GD_CORE_API ExpressionValidator : public ExpressionParser2NodeWorker { "example: \"Your name: \" + VariableString(PlayerName).", node.rightHandSide->location); } else if (node.op != '+') { - RaiseOperatorError( - _("You've used an operator that is not supported. Only + can be used " - "to concatenate texts."), + RaiseOperatorError( + _("You've used an operator that is not supported. Only + can be used " + "to concatenate texts."), ExpressionParserLocation(node.leftHandSide->location.GetEndPosition() + 1, node.location.GetEndPosition())); } } else if (leftType == Type::Object) { @@ -124,7 +124,11 @@ class GD_CORE_API ExpressionValidator : public ExpressionParser2NodeWorker { node.rightHandSide->Visit(*this); const Type rightType = childType; - childType = leftType; + // The type is decided by the first operand, unless it can (`number|string`) + // or should (`unknown`) be refined, in which case we go for the right + // operand (which got visited knowing the type of the first operand, so it's + // equal or strictly more precise than the left operand). + childType = (leftType == Type::Unknown || leftType == Type::NumberOrString) ? leftType : rightType; } void OnVisitUnaryOperatorNode(UnaryOperatorNode& node) override { ReportAnyError(node); @@ -309,8 +313,10 @@ class GD_CORE_API ExpressionValidator : public ExpressionParser2NodeWorker { RaiseTypeError( _("You've entered a name, but this type was expected:") + " " + TypeToString(parentType), node.location); + childType = parentType; + } else { + childType = parentType; } - childType = parentType; } void OnVisitObjectFunctionNameNode(ObjectFunctionNameNode& node) override { ReportAnyError(node); @@ -379,6 +385,16 @@ class GD_CORE_API ExpressionValidator : public ExpressionParser2NodeWorker { RaiseError("invalid_operator", message, location); } + void ReadChildTypeFromVariable(gd::Variable::Type variableType) { + if (variableType == gd::Variable::Number) { + childType = Type::Number; + } else if (variableType == gd::Variable::String) { + childType = Type::String; + } else { + // Nothing - we don't know the precise type (this could be used as a string or as a number). + } + } + static Type StringToType(const gd::String &type); static const gd::String &TypeToString(Type type); static const gd::String unknownTypeString; diff --git a/Core/GDCore/IDE/Events/UsedExtensionsFinder.cpp b/Core/GDCore/IDE/Events/UsedExtensionsFinder.cpp index ca0e4493c0..aeaafa11c4 100644 --- a/Core/GDCore/IDE/Events/UsedExtensionsFinder.cpp +++ b/Core/GDCore/IDE/Events/UsedExtensionsFinder.cpp @@ -110,7 +110,7 @@ void UsedExtensionsFinder::OnVisitVariableNode(VariableNode& node) { result.GetUsedExtensions().insert("BuiltinVariables"); auto type = gd::ExpressionTypeFinder::GetType( - project.GetCurrentPlatform(), GetObjectsContainersList(), rootType, node); + project.GetCurrentPlatform(), GetProjectScopedContainers(), rootType, node); if (gd::ParameterMetadata::IsExpression("variable", type)) { // Nothing to do (this can't reference an object) @@ -154,7 +154,7 @@ void UsedExtensionsFinder::OnVisitVariableBracketAccessorNode( // Add extensions bound to Objects/Behaviors/Functions void UsedExtensionsFinder::OnVisitIdentifierNode(IdentifierNode &node) { auto type = gd::ExpressionTypeFinder::GetType( - project.GetCurrentPlatform(), GetObjectsContainersList(), rootType, node); + project.GetCurrentPlatform(), GetProjectScopedContainers(), rootType, node); if (gd::ParameterMetadata::IsObject(type) || GetObjectsContainersList().HasObjectOrGroupNamed(node.identifierName)) { // An object or object variable is used. diff --git a/Core/GDCore/Project/ObjectsContainersList.cpp b/Core/GDCore/Project/ObjectsContainersList.cpp index 2b6ea62f55..6c8960097d 100644 --- a/Core/GDCore/Project/ObjectsContainersList.cpp +++ b/Core/GDCore/Project/ObjectsContainersList.cpp @@ -148,6 +148,55 @@ ObjectsContainersList::GetObjectOrGroupVariablesContainer( return nullptr; } +gd::Variable::Type ObjectsContainersList::GetTypeOfObjectOrGroupVariable( + const gd::String& objectOrGroupName, const gd::String& variableName) const { + + for (auto it = objectsContainers.rbegin(); it != objectsContainers.rend(); + ++it) { + if ((*it)->HasObjectNamed(objectOrGroupName)) { + const auto& variables = + (*it)->GetObject(objectOrGroupName).GetVariables(); + + return variables.Get(variableName).GetType(); + } + if ((*it)->GetObjectGroups().Has(objectOrGroupName)) { + // This could be adapted if objects groups have variables in the future. + + // Currently, a group is considered as the "intersection" of all of its + // objects. Search "groups is the intersection of its objects" in the + // codebase. Consider that the first object having the variable will + // define its type. + const auto& objectGroup = (*it)->GetObjectGroups().Get(objectOrGroupName); + const auto& objectNames = objectGroup.GetAllObjectsNames(); + + for (const auto& objectName : objectNames) { + if (HasObjectWithVariableNamed(objectName, variableName)) { + return GetTypeOfObjectVariable(objectName, variableName); + } + } + + return Variable::Type::Number; + } + } + + return Variable::Type::Number; +} + +gd::Variable::Type ObjectsContainersList::GetTypeOfObjectVariable(const gd::String& objectName, const gd::String& variableName) const { + + for (auto it = objectsContainers.rbegin(); it != objectsContainers.rend(); + ++it) { + if ((*it)->HasObjectNamed(objectName)) { + const auto& variables = + (*it)->GetObject(objectName).GetVariables(); + + return variables.Get(variableName).GetType(); + } + } + + return Variable::Type::Number; +} + void ObjectsContainersList::ForEachObjectOrGroupVariableWithPrefix( const gd::String& objectOrGroupName, const gd::String& prefix, diff --git a/Core/GDCore/Project/ObjectsContainersList.h b/Core/GDCore/Project/ObjectsContainersList.h index 2c0aaf045e..606e17e9e8 100644 --- a/Core/GDCore/Project/ObjectsContainersList.h +++ b/Core/GDCore/Project/ObjectsContainersList.h @@ -72,6 +72,11 @@ class GD_CORE_API ObjectsContainersList { const gd::VariablesContainer* GetObjectOrGroupVariablesContainer( const gd::String& objectOrGroupName) const; + /** + * \brief Get a type from an object/group variable. + */ + gd::Variable::Type GetTypeOfObjectOrGroupVariable(const gd::String& objectOrGroupName, const gd::String& variableName) const; + /** * \brief Get a type from an object/group name. * \note If a group contains only objects of a same type, then the group has @@ -160,6 +165,8 @@ class GD_CORE_API ObjectsContainersList { bool HasObjectWithVariableNamed(const gd::String& objectName, const gd::String& variableName) const; + gd::Variable::Type GetTypeOfObjectVariable(const gd::String& objectName, const gd::String& variableName) const; + void ForEachObjectVariableWithPrefix( const gd::String& objectOrGroupName, const gd::String& prefix, diff --git a/Core/tests/ExpressionCodeGenerator.cpp b/Core/tests/ExpressionCodeGenerator.cpp index bbb1835ace..989856dc1c 100644 --- a/Core/tests/ExpressionCodeGenerator.cpp +++ b/Core/tests/ExpressionCodeGenerator.cpp @@ -22,12 +22,19 @@ TEST_CASE("ExpressionCodeGenerator", "[common][events]") { auto &layout1 = project.InsertNewLayout("Layout1", 0); // Add some variables and objects: - layout1.GetVariables().InsertNew("MySceneVariable", 0); - layout1.GetVariables().InsertNew("MySceneVariable2", 1); - layout1.GetVariables().InsertNew("MySceneStructureVariable", 2).GetChild("MyChild"); - layout1.GetVariables().InsertNew("MySceneStructureVariable2", 2).GetChild("MyChild"); + project.GetVariables().InsertNew("MyGlobalNumberVariable").SetValue(1234); + project.GetVariables().InsertNew("MyGlobalStringVariable").SetString("TestGlobal"); + layout1.GetVariables().InsertNew("MySceneVariable").SetValue(123); + layout1.GetVariables().InsertNew("MySceneVariable2").SetValue(123); + layout1.GetVariables().InsertNew("MySceneStringVariable").SetString("MyString"); + layout1.GetVariables().InsertNew("MySceneBooleanVariable").SetBool(true); + layout1.GetVariables().InsertNew("MySceneStructureVariable").GetChild("MyChild"); + layout1.GetVariables().InsertNew("MySceneStructureVariable2").GetChild("MyChild"); - layout1.InsertNewObject(project, "MyExtension::Sprite", "MySpriteObject", 0); + auto &mySpriteObject = layout1.InsertNewObject(project, "MyExtension::Sprite", "MySpriteObject", 0); + mySpriteObject.GetVariables().InsertNew("MyNumberVariable").SetValue(123); + mySpriteObject.GetVariables().InsertNew("MyStringVariable").SetString("Test"); + mySpriteObject.GetVariables().InsertNew("MyStructureVariable").GetChild("MyStringChild").SetString("Test"); layout1.InsertNewObject( project, "MyExtension::Sprite", "MyOtherSpriteObject", 1); layout1.InsertNewObject(project, @@ -432,7 +439,7 @@ TEST_CASE("ExpressionCodeGenerator", "[common][events]") { REQUIRE(node); node->Visit(expressionCodeGenerator); - REQUIRE(expressionCodeGenerator.GetOutput() == "getPropertyMyProperty() + 1"); + REQUIRE(expressionCodeGenerator.GetOutput() == "getPropertyMyPropertyAsnumber() + 1"); } { auto node = @@ -444,7 +451,56 @@ TEST_CASE("ExpressionCodeGenerator", "[common][events]") { REQUIRE(node); node->Visit(expressionCodeGenerator); - REQUIRE(expressionCodeGenerator.GetOutput() == "getPropertyMyProperty() + getPropertyMyProperty2()"); + REQUIRE(expressionCodeGenerator.GetOutput() == "getPropertyMyPropertyAsnumber() + getPropertyMyProperty2Asnumber()"); + } + } + SECTION("Properties (1 level, number|string)") { + gd::PropertiesContainer propertiesContainer(gd::EventsFunctionsContainer::Extension); + + auto projectScopedContainersWithProperties = gd::ProjectScopedContainers::MakeNewProjectScopedContainersForProjectAndLayout(project, layout1); + projectScopedContainersWithProperties.AddPropertiesContainer(propertiesContainer); + + propertiesContainer.InsertNew("MyNumberProperty").SetType("Number"); + propertiesContainer.InsertNew("MyStringProperty").SetType("String"); + propertiesContainer.InsertNew("MyBooleanProperty").SetType("Boolean"); + + gd::EventsCodeGenerator codeGeneratorWithProperties(platform, projectScopedContainersWithProperties); + + { + auto node = + parser.ParseExpression("MyNumberProperty"); + gd::ExpressionCodeGenerator expressionCodeGenerator("number|string", + "", + codeGeneratorWithProperties, + context); + + REQUIRE(node); + node->Visit(expressionCodeGenerator); + REQUIRE(expressionCodeGenerator.GetOutput() == "getPropertyMyNumberPropertyAsnumber()"); + } + { + auto node = + parser.ParseExpression("MyStringProperty"); + gd::ExpressionCodeGenerator expressionCodeGenerator("number|string", + "", + codeGeneratorWithProperties, + context); + + REQUIRE(node); + node->Visit(expressionCodeGenerator); + REQUIRE(expressionCodeGenerator.GetOutput() == "getPropertyMyStringPropertyAsstring()"); + } + { + auto node = + parser.ParseExpression("MyBooleanProperty"); + gd::ExpressionCodeGenerator expressionCodeGenerator("number|string", + "", + codeGeneratorWithProperties, + context); + + REQUIRE(node); + node->Visit(expressionCodeGenerator); + REQUIRE(expressionCodeGenerator.GetOutput() == "getPropertyMyBooleanPropertyAsnumber|string()"); } } SECTION("Parameters (1 level)") { @@ -473,7 +529,7 @@ TEST_CASE("ExpressionCodeGenerator", "[common][events]") { REQUIRE(node); node->Visit(expressionCodeGenerator); - REQUIRE(expressionCodeGenerator.GetOutput() == "getParameterMyParameter1() + 1"); + REQUIRE(expressionCodeGenerator.GetOutput() == "getParameterMyParameter1Asnumber() + 1"); } { auto node = @@ -485,7 +541,64 @@ TEST_CASE("ExpressionCodeGenerator", "[common][events]") { REQUIRE(node); node->Visit(expressionCodeGenerator); - REQUIRE(expressionCodeGenerator.GetOutput() == "getParameterMyParameter1() + getParameterMyParameter2()"); + REQUIRE(expressionCodeGenerator.GetOutput() == "getParameterMyParameter1Asnumber() + getParameterMyParameter2Asnumber()"); + } + } + SECTION("Parameters (1 level, number|string)") { + std::vector parameters; + gd::ParameterMetadata param1; + param1.SetName("MyNumberParameter"); + param1.SetType("number"); + gd::ParameterMetadata param2; + param2.SetName("MyStringParameter"); + param2.SetType("string"); + gd::ParameterMetadata param3; + param3.SetName("MyBooleanParameter"); + param3.SetType("yesorno"); + parameters.push_back(param1); + parameters.push_back(param2); + parameters.push_back(param3); + + auto projectScopedContainersWithParameters = gd::ProjectScopedContainers::MakeNewProjectScopedContainersForProjectAndLayout(project, layout1); + projectScopedContainersWithParameters.AddParameters(parameters); + + gd::EventsCodeGenerator codeGeneratorWithProperties(platform, projectScopedContainersWithParameters); + + { + auto node = + parser.ParseExpression("MyNumberParameter"); + gd::ExpressionCodeGenerator expressionCodeGenerator("number|string", + "", + codeGeneratorWithProperties, + context); + + REQUIRE(node); + node->Visit(expressionCodeGenerator); + REQUIRE(expressionCodeGenerator.GetOutput() == "getParameterMyNumberParameterAsnumber()"); + } + { + auto node = + parser.ParseExpression("MyStringParameter"); + gd::ExpressionCodeGenerator expressionCodeGenerator("number|string", + "", + codeGeneratorWithProperties, + context); + + REQUIRE(node); + node->Visit(expressionCodeGenerator); + REQUIRE(expressionCodeGenerator.GetOutput() == "getParameterMyStringParameterAsstring()"); + } + { + auto node = + parser.ParseExpression("MyBooleanParameter"); + gd::ExpressionCodeGenerator expressionCodeGenerator("number|string", + "", + codeGeneratorWithProperties, + context); + + REQUIRE(node); + node->Visit(expressionCodeGenerator); + REQUIRE(expressionCodeGenerator.GetOutput() == "getParameterMyBooleanParameterAsnumber|string()"); } } SECTION("Scene variables (1 level)") { @@ -554,7 +667,7 @@ TEST_CASE("ExpressionCodeGenerator", "[common][events]") { REQUIRE(expressionCodeGenerator.GetOutput() == "getLayoutVariable(MySceneStructureVariable).getChild(\"MyChild\").getAsNumber() + getLayoutVariable(MySceneStructureVariable2).getChild(\"MyChild\").getAsNumber()"); } } - SECTION("Scene variables (2 levels with bracket accessor)") { + SECTION("Scene variables (2 levels with bracket accessor, string)") { { auto node = parser.ParseExpression("MySceneStructureVariable[\"MyChild\"] + 1"); @@ -580,6 +693,144 @@ TEST_CASE("ExpressionCodeGenerator", "[common][events]") { REQUIRE(expressionCodeGenerator.GetOutput() == "getLayoutVariable(MySceneStructureVariable).getChild(\"MyChild\").getAsNumber() + getLayoutVariable(MySceneStructureVariable2).getChild(\"MyChild\").getAsNumber()"); } } + SECTION("Scene variables (2 levels with bracket accessor, number)") { + { + auto node = + parser.ParseExpression("MySceneStructureVariable[3] + 1"); + gd::ExpressionCodeGenerator expressionCodeGenerator("number", + "", + codeGenerator, + context); + + REQUIRE(node); + node->Visit(expressionCodeGenerator); + REQUIRE(expressionCodeGenerator.GetOutput() == "getLayoutVariable(MySceneStructureVariable).getChild(3).getAsNumber() + 1"); + } + { + auto node = + parser.ParseExpression("MySceneStructureVariable[3] + MySceneStructureVariable2[3]"); + gd::ExpressionCodeGenerator expressionCodeGenerator("number", + "", + codeGenerator, + context); + + REQUIRE(node); + node->Visit(expressionCodeGenerator); + REQUIRE(expressionCodeGenerator.GetOutput() == "getLayoutVariable(MySceneStructureVariable).getChild(3).getAsNumber() + getLayoutVariable(MySceneStructureVariable2).getChild(3).getAsNumber()"); + } + } + SECTION("Scene variables (2 levels with bracket accessor, using a number variable as index)") { + { + auto node = + parser.ParseExpression("MySceneStructureVariable[MySceneVariable] + 1"); + gd::ExpressionCodeGenerator expressionCodeGenerator("number", + "", + codeGenerator, + context); + + REQUIRE(node); + node->Visit(expressionCodeGenerator); + REQUIRE(expressionCodeGenerator.GetOutput() == "getLayoutVariable(MySceneStructureVariable).getChild(getLayoutVariable(MySceneVariable).getAsNumber()).getAsNumber() + 1"); + } + } + SECTION("Scene variables (2 levels with bracket accessor, using a string variable as index)") { + { + auto node = + parser.ParseExpression("MySceneStructureVariable[MySceneStringVariable] + 1"); + gd::ExpressionCodeGenerator expressionCodeGenerator("number", + "", + codeGenerator, + context); + + REQUIRE(node); + node->Visit(expressionCodeGenerator); + REQUIRE(expressionCodeGenerator.GetOutput() == "getLayoutVariable(MySceneStructureVariable).getChild(getLayoutVariable(MySceneStringVariable).getAsString()).getAsNumber() + 1"); + } + } + SECTION("Scene variables (2 levels with bracket accessor, using a non string/number variable as index)") { + { + auto node = + parser.ParseExpression("MySceneStructureVariable[MySceneBooleanVariable] + 1"); + gd::ExpressionCodeGenerator expressionCodeGenerator("number", + "", + codeGenerator, + context); + + REQUIRE(node); + node->Visit(expressionCodeGenerator); + REQUIRE(expressionCodeGenerator.GetOutput() == "getLayoutVariable(MySceneStructureVariable).getChild(getLayoutVariable(MySceneBooleanVariable).getAsNumberOrString()).getAsNumber() + 1"); + } + } + SECTION("Scene variables (2 levels with bracket accessor, using a unknown variable type as index)") { + { + auto node = + parser.ParseExpression("MySceneStructureVariable[MySceneStructureVariable.MyChild.CantKnownTheTypeSoStayGeneric] + 1"); + gd::ExpressionCodeGenerator expressionCodeGenerator("number", + "", + codeGenerator, + context); + + REQUIRE(node); + node->Visit(expressionCodeGenerator); + REQUIRE(expressionCodeGenerator.GetOutput() == "getLayoutVariable(MySceneStructureVariable).getChild(getLayoutVariable(MySceneStructureVariable).getChild(\"MyChild\").getChild(\"CantKnownTheTypeSoStayGeneric\").getAsNumberOrString()).getAsNumber() + 1"); + } + } + SECTION("Scene variables (2 levels with bracket accessor, using a unknown variable type and an operator with a number as index)") { + { + auto node = + parser.ParseExpression("MySceneStructureVariable[MySceneStructureVariable.MyChild.CantKnownTheTypeSoStayGeneric + 2] + 1"); + gd::ExpressionCodeGenerator expressionCodeGenerator("number", + "", + codeGenerator, + context); + + REQUIRE(node); + node->Visit(expressionCodeGenerator); + REQUIRE(expressionCodeGenerator.GetOutput() == "getLayoutVariable(MySceneStructureVariable).getChild(getLayoutVariable(MySceneStructureVariable).getChild(\"MyChild\").getChild(\"CantKnownTheTypeSoStayGeneric\").getAsNumber() + 2).getAsNumber() + 1"); + } + } + SECTION("Scene variables (2 levels with bracket accessor, using a unknown variable type and an operator with a string as index)") { + { + auto node = + parser.ParseExpression("MySceneStructureVariable[MySceneStructureVariable.MyChild.CantKnownTheTypeSoStayGeneric + \"Test\"] + 1"); + gd::ExpressionCodeGenerator expressionCodeGenerator("number", + "", + codeGenerator, + context); + + REQUIRE(node); + node->Visit(expressionCodeGenerator); + REQUIRE(expressionCodeGenerator.GetOutput() == "getLayoutVariable(MySceneStructureVariable).getChild(getLayoutVariable(MySceneStructureVariable).getChild(\"MyChild\").getChild(\"CantKnownTheTypeSoStayGeneric\").getAsString() + \"Test\").getAsNumber() + 1"); + } + } + SECTION("Scene variables (2 levels with bracket accessor, using a unknown variable type as index) (expression type: number|string)") { + { + auto node = + parser.ParseExpression("MySceneStructureVariable[MySceneStructureVariable.MyChild.CantKnownTheTypeSoStayGeneric]"); + gd::ExpressionCodeGenerator expressionCodeGenerator("number|string", + "", + codeGenerator, + context); + + REQUIRE(node); + node->Visit(expressionCodeGenerator); + REQUIRE(expressionCodeGenerator.GetOutput() == "getLayoutVariable(MySceneStructureVariable).getChild(getLayoutVariable(MySceneStructureVariable).getChild(\"MyChild\").getChild(\"CantKnownTheTypeSoStayGeneric\").getAsNumberOrString()).getAsNumberOrString()"); + } + } + SECTION("Scene variables (2 levels with bracket accessor, using a number variable casted to string as index)") { + { + auto node = + parser.ParseExpression("MySceneStructureVariable[\"\" + MySceneVariable] + 1"); + gd::ExpressionCodeGenerator expressionCodeGenerator("number", + "", + codeGenerator, + context); + + REQUIRE(node); + node->Visit(expressionCodeGenerator); + REQUIRE(expressionCodeGenerator.GetOutput() == "getLayoutVariable(MySceneStructureVariable).getChild(\"\" + getLayoutVariable(MySceneVariable).getAsString()).getAsNumber() + 1"); + } + } SECTION("Object variable with non existing object (invalid)") { auto node = parser.ParseExpression("MyNonExistingSpriteObject.MyVariable"); @@ -757,7 +1008,20 @@ TEST_CASE("ExpressionCodeGenerator", "[common][events]") { "fakeBadVariable"); } } - SECTION("Valid variables") { + SECTION("Valid variables (upcoming, new 'variable' type working for any variable)") { + // When implemented, copy the test cases from the next section, like this: + // SECTION("simple variable") { + // REQUIRE(gd::ExpressionCodeGenerator::GenerateExpressionCode( + // codeGenerator, context, "variable", "MySceneVariable", "") + // == "getLayoutVariable(MySceneVariable)"); + // } + // SECTION("simple (global) variable") { + // REQUIRE(gd::ExpressionCodeGenerator::GenerateExpressionCode( + // codeGenerator, context, "variable", "MyGlobalNumberVariable", "") + // == "getProjectVariable(MyGlobalNumberVariable)"); + // } + } + SECTION("Valid variables (legacy, pre-scoped variables)") { SECTION("simple variable") { REQUIRE(gd::ExpressionCodeGenerator::GenerateExpressionCode( codeGenerator, context, "scenevar", "myVariable", "") @@ -779,13 +1043,63 @@ TEST_CASE("ExpressionCodeGenerator", "[common][events]") { "\"world\" ]", "") == "getLayoutVariable(myVariable).getChild(\"hello\" + \"world\")"); } - SECTION("object variable (legacy)") { + SECTION("bracket access (using a string object variable inside)") { + REQUIRE(gd::ExpressionCodeGenerator::GenerateExpressionCode( + codeGenerator, context, "scenevar", "myVariable[MySpriteObject.MyStringVariable]", "") + == "getLayoutVariable(myVariable).getChild(getVariableForObject(MySpriteObject, MyStringVariable).getAsString())"); + } + SECTION("bracket access (using a number object variable inside)") { + REQUIRE(gd::ExpressionCodeGenerator::GenerateExpressionCode( + codeGenerator, context, "scenevar", "myVariable[MySpriteObject.MyNumberVariable]", "") + == "getLayoutVariable(myVariable).getChild(getVariableForObject(MySpriteObject, MyNumberVariable).getAsNumber())"); + } + SECTION("bracket access (using a string variable inside)") { + REQUIRE(gd::ExpressionCodeGenerator::GenerateExpressionCode( + codeGenerator, context, "scenevar", "myVariable[MySceneStringVariable]", "") + == "getLayoutVariable(myVariable).getChild(getLayoutVariable(MySceneStringVariable).getAsString())"); + } + SECTION("bracket access (using a number variable inside)") { + REQUIRE(gd::ExpressionCodeGenerator::GenerateExpressionCode( + codeGenerator, context, "scenevar", "myVariable[MySceneVariable]", "") + == "getLayoutVariable(myVariable).getChild(getLayoutVariable(MySceneVariable).getAsNumber())"); + } + SECTION("bracket access (using a string global variable inside)") { + REQUIRE(gd::ExpressionCodeGenerator::GenerateExpressionCode( + codeGenerator, context, "scenevar", "myVariable[MyGlobalStringVariable]", "") + == "getLayoutVariable(myVariable).getChild(getProjectVariable(MyGlobalStringVariable).getAsString())"); + } + SECTION("bracket access (using a number global variable inside)") { + REQUIRE(gd::ExpressionCodeGenerator::GenerateExpressionCode( + codeGenerator, context, "scenevar", "myVariable[MyGlobalNumberVariable]", "") + == "getLayoutVariable(myVariable).getChild(getProjectVariable(MyGlobalNumberVariable).getAsNumber())"); + } + SECTION("bracket access (using a boolean variable inside)") { + REQUIRE(gd::ExpressionCodeGenerator::GenerateExpressionCode( + codeGenerator, context, "scenevar", "myVariable[MySceneBooleanVariable]", "") + == "getLayoutVariable(myVariable).getChild(getLayoutVariable(MySceneBooleanVariable).getAsNumberOrString())"); + } + SECTION("bracket access (using a structure variable inside)") { + REQUIRE(gd::ExpressionCodeGenerator::GenerateExpressionCode( + codeGenerator, context, "scenevar", "myVariable[MySceneStructureVariable.MyChild.SubChild]", "") + == "getLayoutVariable(myVariable).getChild(getLayoutVariable(MySceneStructureVariable).getChild(\"MyChild\").getChild(\"SubChild\").getAsNumberOrString())"); + } + SECTION("object variable") { REQUIRE(gd::ExpressionCodeGenerator::GenerateExpressionCode( codeGenerator, context, "objectvar", "myVariable", "MySpriteObject") == "getVariableForObject(MySpriteObject, myVariable)"); } + SECTION("object variable with bracket access (using a structure variable inside)") { + REQUIRE(gd::ExpressionCodeGenerator::GenerateExpressionCode( + codeGenerator, context, "objectvar", "myVariable[MySceneStringVariable]", "MySpriteObject") + == "getVariableForObject(MySpriteObject, myVariable).getChild(getLayoutVariable(MySceneStringVariable).getAsString())"); + } + SECTION("object variable with bracket access (using an object variable inside)") { + REQUIRE(gd::ExpressionCodeGenerator::GenerateExpressionCode( + codeGenerator, context, "objectvar", "myVariable[MySpriteObject.MyStructureVariable.MyChild]", "MySpriteObject") + == "getVariableForObject(MySpriteObject, myVariable).getChild(getVariableForObject(MySpriteObject, MyStructureVariable).getChild(\"MyChild\").getAsNumberOrString())"); + } } - SECTION("Valid function calls with variables") { + SECTION("Valid function calls with variables (legacy, pre-scoped)") { SECTION("Simple access") { SECTION("Scene variable") { auto node = parser.ParseExpression( diff --git a/Core/tests/ExpressionParser2.cpp b/Core/tests/ExpressionParser2.cpp index 309f07d72c..19c6c6ea4f 100644 --- a/Core/tests/ExpressionParser2.cpp +++ b/Core/tests/ExpressionParser2.cpp @@ -22,10 +22,13 @@ TEST_CASE("ExpressionParser2", "[common][events]") { gd::Platform platform; SetupProjectWithDummyPlatform(project, platform); auto &layout1 = project.InsertNewLayout("Layout1", 0); - layout1.GetVariables().InsertNew("MySceneVariable", 0); - layout1.GetVariables().InsertNew("MySceneVariable2", 1); - layout1.GetVariables().InsertNew("MySceneStructureVariable", 2).GetChild("MyChild"); - layout1.GetVariables().InsertNew("MySceneStructureVariable2", 2).GetChild("MyChild"); + layout1.GetVariables().InsertNew("MySceneVariable"); + layout1.GetVariables().InsertNew("MySceneVariable2"); + layout1.GetVariables().InsertNew("MySceneStructureVariable").GetChild("MyChild"); + layout1.GetVariables().InsertNew("MySceneStructureVariable2").GetChild("MyChild"); + layout1.GetVariables().InsertNew("MySceneNumberVariable").SetValue(123); + layout1.GetVariables().InsertNew("MySceneStringVariable").SetString("Test"); + layout1.GetVariables().InsertNew("MySceneBooleanVariable").SetBool(true); // Create an instance of BuiltinObject. // This is not possible in practice. @@ -38,9 +41,11 @@ TEST_CASE("ExpressionParser2", "[common][events]") { layout1.GetObjectGroups().InsertNew("EmptyGroup"); auto &mySpriteObject = layout1.InsertNewObject(project, "MyExtension::Sprite", "MySpriteObject", 1); - mySpriteObject.GetVariables().InsertNew("MyVariable", 0); - mySpriteObject.GetVariables().InsertNew("MyVariable2", 1); - mySpriteObject.GetVariables().InsertNew("MyVariable3", 2); + mySpriteObject.GetVariables().InsertNew("MyVariable"); + mySpriteObject.GetVariables().InsertNew("MyVariable2"); + mySpriteObject.GetVariables().InsertNew("MyVariable3"); + mySpriteObject.GetVariables().InsertNew("MyNumberVariable").SetValue(123); + mySpriteObject.GetVariables().InsertNew("MyStringVariable").SetString("Test"); auto &mySpriteObject2 = layout1.InsertNewObject(project, "MyExtension::Sprite", "MySpriteObject2", 1); mySpriteObject2.GetVariables().InsertNew("MyVariable", 0); mySpriteObject2.GetVariables().InsertNew("MyVariable2", 1); @@ -64,7 +69,7 @@ TEST_CASE("ExpressionParser2", "[common][events]") { REQUIRE(node != nullptr); auto &emptyNode = dynamic_cast(*node); auto type = gd::ExpressionTypeFinder::GetType( - platform, objectsContainersList, "string", emptyNode); + platform, projectScopedContainers, "string", emptyNode); REQUIRE(type == "string"); REQUIRE(emptyNode.text == ""); @@ -79,7 +84,7 @@ TEST_CASE("ExpressionParser2", "[common][events]") { REQUIRE(node != nullptr); auto &emptyNode = dynamic_cast(*node); auto type = gd::ExpressionTypeFinder::GetType( - platform, objectsContainersList, "number", emptyNode); + platform, projectScopedContainers, "number", emptyNode); REQUIRE(type == "number"); REQUIRE(emptyNode.text == ""); @@ -94,7 +99,7 @@ TEST_CASE("ExpressionParser2", "[common][events]") { REQUIRE(node != nullptr); auto &emptyNode = dynamic_cast(*node); auto type = gd::ExpressionTypeFinder::GetType( - platform, objectsContainersList, "object", emptyNode); + platform, projectScopedContainers, "object", emptyNode); REQUIRE(type == "object"); REQUIRE(emptyNode.text == ""); @@ -111,7 +116,7 @@ TEST_CASE("ExpressionParser2", "[common][events]") { REQUIRE(node != nullptr); auto &emptyNode = dynamic_cast(*node); auto type = gd::ExpressionTypeFinder::GetType( - platform, objectsContainersList, "string", emptyNode); + platform, projectScopedContainers, "string", emptyNode); REQUIRE(type == "string"); REQUIRE(emptyNode.text == ""); } @@ -120,7 +125,7 @@ TEST_CASE("ExpressionParser2", "[common][events]") { REQUIRE(node != nullptr); auto &emptyNode = dynamic_cast(*node); auto type = gd::ExpressionTypeFinder::GetType( - platform, objectsContainersList, "number", emptyNode); + platform, projectScopedContainers, "number", emptyNode); REQUIRE(type == "number"); REQUIRE(emptyNode.text == ""); } @@ -129,7 +134,7 @@ TEST_CASE("ExpressionParser2", "[common][events]") { REQUIRE(node != nullptr); auto &emptyNode = dynamic_cast(*node); auto type = gd::ExpressionTypeFinder::GetType( - platform, objectsContainersList, "object", emptyNode); + platform, projectScopedContainers, "object", emptyNode); REQUIRE(type == "object"); REQUIRE(emptyNode.text == ""); } @@ -485,7 +490,7 @@ TEST_CASE("ExpressionParser2", "[common][events]") { REQUIRE(node != nullptr); auto &operatorNode = dynamic_cast(*node); auto type = gd::ExpressionTypeFinder::GetType( - platform, objectsContainersList, "number", operatorNode); + platform, projectScopedContainers, "number", operatorNode); REQUIRE(operatorNode.op == '+'); REQUIRE(type == "number"); auto &leftNumberNode = @@ -504,7 +509,7 @@ TEST_CASE("ExpressionParser2", "[common][events]") { REQUIRE(node != nullptr); auto &operatorNode = dynamic_cast(*node); auto type = gd::ExpressionTypeFinder::GetType( - platform, objectsContainersList, "string", operatorNode); + platform, projectScopedContainers, "string", operatorNode); REQUIRE(operatorNode.op == '+'); REQUIRE(type == "string"); auto &leftTextNode = @@ -526,7 +531,7 @@ TEST_CASE("ExpressionParser2", "[common][events]") { REQUIRE(node != nullptr); auto &operatorNode = dynamic_cast(*node); auto type = gd::ExpressionTypeFinder::GetType( - platform, objectsContainersList, "number|string", operatorNode); + platform, projectScopedContainers, "number|string", operatorNode); REQUIRE(operatorNode.op == '+'); REQUIRE(type == "number"); auto &leftNumberNode = @@ -545,7 +550,7 @@ TEST_CASE("ExpressionParser2", "[common][events]") { REQUIRE(node != nullptr); auto &operatorNode = dynamic_cast(*node); auto type = gd::ExpressionTypeFinder::GetType( - platform, objectsContainersList, "number|string", operatorNode); + platform, projectScopedContainers, "number|string", operatorNode); REQUIRE(operatorNode.op == '+'); REQUIRE(type == "string"); auto &leftTextNode = @@ -567,7 +572,7 @@ TEST_CASE("ExpressionParser2", "[common][events]") { REQUIRE(node != nullptr); auto &unaryOperatorNode = dynamic_cast(*node); auto type = gd::ExpressionTypeFinder::GetType( - platform, objectsContainersList, "number", unaryOperatorNode); + platform, projectScopedContainers, "number", unaryOperatorNode); REQUIRE(unaryOperatorNode.op == '-'); REQUIRE(type == "number"); auto &numberNode = @@ -583,7 +588,7 @@ TEST_CASE("ExpressionParser2", "[common][events]") { REQUIRE(node != nullptr); auto &unaryOperatorNode = dynamic_cast(*node); auto type = gd::ExpressionTypeFinder::GetType( - platform, objectsContainersList, "number", unaryOperatorNode); + platform, projectScopedContainers, "number", unaryOperatorNode); REQUIRE(unaryOperatorNode.op == '+'); REQUIRE(type == "number"); auto &numberNode = @@ -599,7 +604,7 @@ TEST_CASE("ExpressionParser2", "[common][events]") { REQUIRE(node != nullptr); auto &unaryOperatorNode = dynamic_cast(*node); auto type = gd::ExpressionTypeFinder::GetType( - platform, objectsContainersList, "number", unaryOperatorNode); + platform, projectScopedContainers, "number", unaryOperatorNode); REQUIRE(unaryOperatorNode.op == '-'); REQUIRE(type == "number"); auto &numberNode = @@ -617,7 +622,7 @@ TEST_CASE("ExpressionParser2", "[common][events]") { REQUIRE(node != nullptr); auto &unaryOperatorNode = dynamic_cast(*node); auto type = gd::ExpressionTypeFinder::GetType( - platform, objectsContainersList, "number|string", unaryOperatorNode); + platform, projectScopedContainers, "number|string", unaryOperatorNode); REQUIRE(unaryOperatorNode.op == '-'); REQUIRE(type == "number"); auto &numberNode = @@ -633,7 +638,7 @@ TEST_CASE("ExpressionParser2", "[common][events]") { REQUIRE(node != nullptr); auto &unaryOperatorNode = dynamic_cast(*node); auto type = gd::ExpressionTypeFinder::GetType( - platform, objectsContainersList, "number|string", unaryOperatorNode); + platform, projectScopedContainers, "number|string", unaryOperatorNode); REQUIRE(unaryOperatorNode.op == '+'); REQUIRE(type == "number"); auto &numberNode = @@ -649,7 +654,7 @@ TEST_CASE("ExpressionParser2", "[common][events]") { REQUIRE(node != nullptr); auto &unaryOperatorNode = dynamic_cast(*node); auto type = gd::ExpressionTypeFinder::GetType( - platform, objectsContainersList, "number|string", unaryOperatorNode); + platform, projectScopedContainers, "number|string", unaryOperatorNode); REQUIRE(unaryOperatorNode.op == '-'); REQUIRE(type == "number"); auto &numberNode = @@ -914,6 +919,242 @@ TEST_CASE("ExpressionParser2", "[common][events]") { REQUIRE(validator.GetFatalErrors()[0]->GetEndPosition() == 19); } } + SECTION("Numbers and texts mismatches ('number|string' type, with a known variable type first)") { + { + auto node = + parser.ParseExpression("MySceneNumberVariable + \"hello world\""); + REQUIRE(node != nullptr); + + gd::ExpressionValidator validator(platform, projectScopedContainers, "number|string"); + node->Visit(validator); + REQUIRE(validator.GetFatalErrors().size() == 1); + REQUIRE(validator.GetFatalErrors()[0]->GetMessage() == + "You entered a text, but a number was expected."); + REQUIRE(validator.GetFatalErrors()[0]->GetStartPosition() == 24); + } + { + auto node = + parser.ParseExpression("MySceneStringVariable + 123"); + REQUIRE(node != nullptr); + + gd::ExpressionValidator validator(platform, projectScopedContainers, "number|string"); + node->Visit(validator); + REQUIRE(validator.GetFatalErrors().size() == 1); + REQUIRE(validator.GetFatalErrors()[0]->GetMessage() == + "You entered a number, but a text was expected (in quotes)."); + REQUIRE(validator.GetFatalErrors()[0]->GetStartPosition() == 24); + } + { + auto node = + parser.ParseExpression("MySceneNumberVariable + MySceneBooleanVariable + \"hello world\""); + REQUIRE(node != nullptr); + + gd::ExpressionValidator validator(platform, projectScopedContainers, "number|string"); + node->Visit(validator); + REQUIRE(validator.GetFatalErrors().size() == 1); + REQUIRE(validator.GetFatalErrors()[0]->GetMessage() == + "You entered a text, but a number was expected."); + REQUIRE(validator.GetFatalErrors()[0]->GetStartPosition() == 49); + } + { + auto node = + parser.ParseExpression("MySceneStringVariable + MySceneBooleanVariable + 123"); + REQUIRE(node != nullptr); + + gd::ExpressionValidator validator(platform, projectScopedContainers, "number|string"); + node->Visit(validator); + REQUIRE(validator.GetFatalErrors().size() == 1); + REQUIRE(validator.GetFatalErrors()[0]->GetMessage() == + "You entered a number, but a text was expected (in quotes)."); + REQUIRE(validator.GetFatalErrors()[0]->GetStartPosition() == 49); + } + { + auto node = + parser.ParseExpression("MySpriteObject.MyNumberVariable + \"hello world\""); + REQUIRE(node != nullptr); + + gd::ExpressionValidator validator(platform, projectScopedContainers, "number|string"); + node->Visit(validator); + REQUIRE(validator.GetFatalErrors().size() == 1); + REQUIRE(validator.GetFatalErrors()[0]->GetMessage() == + "You entered a text, but a number was expected."); + } + { + auto node = + parser.ParseExpression("MySpriteObject.MyStringVariable + 123"); + REQUIRE(node != nullptr); + + gd::ExpressionValidator validator(platform, projectScopedContainers, "number|string"); + node->Visit(validator); + REQUIRE(validator.GetFatalErrors().size() == 1); + REQUIRE(validator.GetFatalErrors()[0]->GetMessage() == + "You entered a number, but a text was expected (in quotes)."); + } + } + SECTION("Numbers and texts mismatches ('number|string' type, with a parameter first)") { + std::vector parameters; + { + gd::ParameterMetadata param; + param.SetName("MyNumberParameter"); + param.SetType("number"); + parameters.push_back(param); + } + { + gd::ParameterMetadata param; + param.SetName("MyStringParameter"); + param.SetType("string"); + parameters.push_back(param); + } + { + gd::ParameterMetadata param; + param.SetName("MyBooleanParameter"); + param.SetType("yesorno"); + parameters.push_back(param); + } + + auto projectScopedContainersWithParameters = gd::ProjectScopedContainers::MakeNewProjectScopedContainersForProjectAndLayout(project, layout1); + projectScopedContainersWithParameters.AddParameters(parameters); + { + auto node = + parser.ParseExpression("MyNumberParameter + \"hello world\""); + REQUIRE(node != nullptr); + + gd::ExpressionValidator validator(platform, projectScopedContainersWithParameters, "number|string"); + node->Visit(validator); + REQUIRE(validator.GetFatalErrors().size() == 1); + REQUIRE(validator.GetFatalErrors()[0]->GetMessage() == + "You entered a text, but a number was expected."); + } + { + auto node = + parser.ParseExpression("MyStringParameter + 123"); + REQUIRE(node != nullptr); + + gd::ExpressionValidator validator(platform, projectScopedContainersWithParameters, "number|string"); + node->Visit(validator); + REQUIRE(validator.GetFatalErrors().size() == 1); + REQUIRE(validator.GetFatalErrors()[0]->GetMessage() == + "You entered a number, but a text was expected (in quotes)."); + } + { + auto node = + parser.ParseExpression("MyNumberParameter + MyBooleanParameter + \"hello world\""); + REQUIRE(node != nullptr); + + gd::ExpressionValidator validator(platform, projectScopedContainersWithParameters, "number|string"); + node->Visit(validator); + REQUIRE(validator.GetFatalErrors().size() == 1); + REQUIRE(validator.GetFatalErrors()[0]->GetMessage() == + "You entered a text, but a number was expected."); + } + { + auto node = + parser.ParseExpression("MyStringParameter + MyBooleanParameter + 123"); + REQUIRE(node != nullptr); + + gd::ExpressionValidator validator(platform, projectScopedContainersWithParameters, "number|string"); + node->Visit(validator); + REQUIRE(validator.GetFatalErrors().size() == 1); + REQUIRE(validator.GetFatalErrors()[0]->GetMessage() == + "You entered a number, but a text was expected (in quotes)."); + } + } + SECTION("Numbers and texts mismatches ('number|string' type, with a property first)") { + gd::PropertiesContainer propertiesContainer(gd::EventsFunctionsContainer::Extension); + + auto projectScopedContainersWithProperties = gd::ProjectScopedContainers::MakeNewProjectScopedContainersForProjectAndLayout(project, layout1); + projectScopedContainersWithProperties.AddPropertiesContainer(propertiesContainer); + + propertiesContainer.InsertNew("MyNumberProperty").SetType("Number"); + propertiesContainer.InsertNew("MyStringProperty").SetType("String"); + propertiesContainer.InsertNew("MyBooleanProperty").SetType("Boolean"); + { + auto node = + parser.ParseExpression("MyNumberProperty + \"hello world\""); + REQUIRE(node != nullptr); + + gd::ExpressionValidator validator(platform, projectScopedContainersWithProperties, "number|string"); + node->Visit(validator); + REQUIRE(validator.GetFatalErrors().size() == 1); + REQUIRE(validator.GetFatalErrors()[0]->GetMessage() == + "You entered a text, but a number was expected."); + } + { + auto node = + parser.ParseExpression("MyStringProperty + 123"); + REQUIRE(node != nullptr); + + gd::ExpressionValidator validator(platform, projectScopedContainersWithProperties, "number|string"); + node->Visit(validator); + REQUIRE(validator.GetFatalErrors().size() == 1); + REQUIRE(validator.GetFatalErrors()[0]->GetMessage() == + "You entered a number, but a text was expected (in quotes)."); + } + { + auto node = + parser.ParseExpression("MyNumberProperty + MyBooleanProperty + \"hello world\""); + REQUIRE(node != nullptr); + + gd::ExpressionValidator validator(platform, projectScopedContainersWithProperties, "number|string"); + node->Visit(validator); + REQUIRE(validator.GetFatalErrors().size() == 1); + REQUIRE(validator.GetFatalErrors()[0]->GetMessage() == + "You entered a text, but a number was expected."); + } + { + auto node = + parser.ParseExpression("MyStringProperty + MyBooleanProperty + 123"); + REQUIRE(node != nullptr); + + gd::ExpressionValidator validator(platform, projectScopedContainersWithProperties, "number|string"); + node->Visit(validator); + REQUIRE(validator.GetFatalErrors().size() == 1); + REQUIRE(validator.GetFatalErrors()[0]->GetMessage() == + "You entered a number, but a text was expected (in quotes)."); + } + } + SECTION("Numbers and texts mismatches ('number|string' type, with an unknown variable type first)") { + { + auto node = parser.ParseExpression("MySceneBooleanVariable + 123 + \"hello world\""); + REQUIRE(node != nullptr); + + gd::ExpressionValidator validator(platform, projectScopedContainers, "number|string"); + node->Visit(validator); + REQUIRE(validator.GetFatalErrors().size() == 1); + REQUIRE(validator.GetFatalErrors()[0]->GetMessage() == + "You entered a text, but a number was expected."); + } + { + auto node = parser.ParseExpression("MySceneBooleanVariable + \"hello world\" + 123"); + REQUIRE(node != nullptr); + + gd::ExpressionValidator validator(platform, projectScopedContainers, "number|string"); + node->Visit(validator); + REQUIRE(validator.GetFatalErrors().size() == 1); + REQUIRE(validator.GetFatalErrors()[0]->GetMessage() == + "You entered a number, but a text was expected (in quotes)."); + } + { + auto node = parser.ParseExpression("MySceneStructureVariable.MyChild.UnknownSubChild + 123 + \"hello world\""); + REQUIRE(node != nullptr); + + gd::ExpressionValidator validator(platform, projectScopedContainers, "number|string"); + node->Visit(validator); + REQUIRE(validator.GetFatalErrors().size() == 1); + REQUIRE(validator.GetFatalErrors()[0]->GetMessage() == + "You entered a text, but a number was expected."); + } + { + auto node = parser.ParseExpression("MySceneStructureVariable.MyChild.UnknownSubChild + \"hello world\" + 123"); + REQUIRE(node != nullptr); + + gd::ExpressionValidator validator(platform, projectScopedContainers, "number|string"); + node->Visit(validator); + REQUIRE(validator.GetFatalErrors().size() == 1); + REQUIRE(validator.GetFatalErrors()[0]->GetMessage() == + "You entered a number, but a text was expected (in quotes)."); + } + } SECTION("Numbers and texts mismatches with parenthesis") { { auto node = @@ -1451,21 +1692,78 @@ TEST_CASE("ExpressionParser2", "[common][events]") { } SECTION("Valid property") { + gd::PropertiesContainer propertiesContainer(gd::EventsFunctionsContainer::Extension); + + auto projectScopedContainersWithProperties = gd::ProjectScopedContainers::MakeNewProjectScopedContainersForProjectAndLayout(project, layout1); + projectScopedContainersWithProperties.AddPropertiesContainer(propertiesContainer); + + propertiesContainer.InsertNew("MyProperty").SetType("Number"); + propertiesContainer.InsertNew("MyProperty2").SetType("String"); + propertiesContainer.InsertNew("MyProperty3").SetType("Boolean"); + { - gd::PropertiesContainer propertiesContainer(gd::EventsFunctionsContainer::Extension); + auto node = + parser.ParseExpression("MyProperty"); - auto projectScopedContainersWithProperties = gd::ProjectScopedContainers::MakeNewProjectScopedContainersForProjectAndLayout(project, layout1); - projectScopedContainersWithProperties.AddPropertiesContainer(propertiesContainer); + gd::ExpressionValidator validator(platform, projectScopedContainersWithProperties, "number|string"); + node->Visit(validator); + REQUIRE(validator.GetFatalErrors().size() == 0); - propertiesContainer.InsertNew("MyProperty"); - propertiesContainer.InsertNew("MyProperty2"); + auto type = gd::ExpressionTypeFinder::GetType( + platform, projectScopedContainersWithProperties, "number|string", *node.get()); + REQUIRE(type == "number"); + } + { + auto node = + parser.ParseExpression("MyProperty2"); + + gd::ExpressionValidator validator(platform, projectScopedContainersWithProperties, "number|string"); + node->Visit(validator); + REQUIRE(validator.GetFatalErrors().size() == 0); + + auto type = gd::ExpressionTypeFinder::GetType( + platform, projectScopedContainersWithProperties, "number|string", *node.get()); + REQUIRE(type == "string"); + } + + { + auto node = + parser.ParseExpression("MyProperty3"); + + gd::ExpressionValidator validator(platform, projectScopedContainersWithProperties, "number|string"); + node->Visit(validator); + REQUIRE(validator.GetFatalErrors().size() == 0); + + auto type = gd::ExpressionTypeFinder::GetType( + platform, projectScopedContainersWithProperties, "number|string", *node.get()); + REQUIRE(type == "number|string"); + } + + { auto node = parser.ParseExpression("MyProperty + MyProperty2"); gd::ExpressionValidator validator(platform, projectScopedContainersWithProperties, "number|string"); node->Visit(validator); REQUIRE(validator.GetFatalErrors().size() == 0); + + auto type = gd::ExpressionTypeFinder::GetType( + platform, projectScopedContainersWithProperties, "number|string", *node.get()); + REQUIRE(type == "number"); + } + + { + auto node = + parser.ParseExpression("MyProperty + MyProperty2 + MyProperty3"); + + gd::ExpressionValidator validator(platform, projectScopedContainersWithProperties, "number|string"); + node->Visit(validator); + REQUIRE(validator.GetFatalErrors().size() == 0); + + auto type = gd::ExpressionTypeFinder::GetType( + platform, projectScopedContainersWithProperties, "number|string", *node.get()); + REQUIRE(type == "number"); } } @@ -1485,7 +1783,7 @@ TEST_CASE("ExpressionParser2", "[common][events]") { gd::ExpressionValidator validator(platform, projectScopedContainersWithProperties, "number|string"); node->Visit(validator); REQUIRE(validator.GetFatalErrors().size() == 1); - REQUIRE(validator.GetFatalErrors()[0]->GetMessage() == "You must enter a number or a text, wrapped inside double quotes (example: \"Hello world\"), or a variable name."); + REQUIRE(validator.GetFatalErrors()[0]->GetMessage() == "You must wrap your text inside double quotes (example: \"Hello world\")."); } } @@ -1529,7 +1827,6 @@ TEST_CASE("ExpressionParser2", "[common][events]") { } SECTION("Valid parameter") { - { std::vector parameters; gd::ParameterMetadata param1; param1.SetName("MyParameter1"); @@ -1537,18 +1834,75 @@ TEST_CASE("ExpressionParser2", "[common][events]") { gd::ParameterMetadata param2; param2.SetName("MyParameter2"); param2.SetType("string"); + gd::ParameterMetadata param3; + param3.SetName("MyParameter3"); + param3.SetType("yesorno"); parameters.push_back(param1); parameters.push_back(param2); + parameters.push_back(param3); auto projectScopedContainersWithParameters = gd::ProjectScopedContainers::MakeNewProjectScopedContainersForProjectAndLayout(project, layout1); projectScopedContainersWithParameters.AddParameters(parameters); + { + auto node = + parser.ParseExpression("MyParameter1"); + + gd::ExpressionValidator validator(platform, projectScopedContainersWithParameters, "number|string"); + node->Visit(validator); + REQUIRE(validator.GetFatalErrors().size() == 0); + + auto type = gd::ExpressionTypeFinder::GetType( + platform, projectScopedContainersWithParameters, "number|string", *node.get()); + REQUIRE(type == "number"); + } + { + auto node = + parser.ParseExpression("MyParameter2"); + + gd::ExpressionValidator validator(platform, projectScopedContainersWithParameters, "number|string"); + node->Visit(validator); + REQUIRE(validator.GetFatalErrors().size() == 0); + + auto type = gd::ExpressionTypeFinder::GetType( + platform, projectScopedContainersWithParameters, "number|string", *node.get()); + REQUIRE(type == "string"); + } + { + auto node = + parser.ParseExpression("MyParameter3"); + + gd::ExpressionValidator validator(platform, projectScopedContainersWithParameters, "number|string"); + node->Visit(validator); + REQUIRE(validator.GetFatalErrors().size() == 0); + + auto type = gd::ExpressionTypeFinder::GetType( + platform, projectScopedContainersWithParameters, "number|string", *node.get()); + REQUIRE(type == "number|string"); + } + { auto node = parser.ParseExpression("MyParameter1 + MyParameter2"); gd::ExpressionValidator validator(platform, projectScopedContainersWithParameters, "number|string"); node->Visit(validator); REQUIRE(validator.GetFatalErrors().size() == 0); + + auto type = gd::ExpressionTypeFinder::GetType( + platform, projectScopedContainersWithParameters, "number|string", *node.get()); + REQUIRE(type == "number"); + } + { + auto node = + parser.ParseExpression("MyParameter1 + MyParameter2 + MyParameter3"); + + gd::ExpressionValidator validator(platform, projectScopedContainersWithParameters, "number|string"); + node->Visit(validator); + REQUIRE(validator.GetFatalErrors().size() == 0); + + auto type = gd::ExpressionTypeFinder::GetType( + platform, projectScopedContainersWithParameters, "number|string", *node.get()); + REQUIRE(type == "number"); } } @@ -1598,7 +1952,7 @@ TEST_CASE("ExpressionParser2", "[common][events]") { gd::ExpressionValidator validator(platform, projectScopedContainersWithParameters, "number|string"); node->Visit(validator); REQUIRE(validator.GetFatalErrors().size() == 1); - REQUIRE(validator.GetFatalErrors()[0]->GetMessage() == "You must enter a number or a text, wrapped inside double quotes (example: \"Hello world\"), or a variable name."); + REQUIRE(validator.GetFatalErrors()[0]->GetMessage() == "You must enter a number."); } } @@ -1658,7 +2012,7 @@ TEST_CASE("ExpressionParser2", "[common][events]") { REQUIRE(node != nullptr); auto &functionNode = dynamic_cast(*node); auto type = gd::ExpressionTypeFinder::GetType( - platform, objectsContainersList, "number|string", functionNode); + platform, projectScopedContainers, "number|string", functionNode); REQUIRE(functionNode.functionName == "MyExtension::GetNumber"); REQUIRE(type == "number"); REQUIRE(functionNode.objectName == ""); @@ -1674,7 +2028,7 @@ TEST_CASE("ExpressionParser2", "[common][events]") { REQUIRE(node != nullptr); auto &functionNode = dynamic_cast(*node); auto type = gd::ExpressionTypeFinder::GetType( - platform, objectsContainersList, "number|string", functionNode); + platform, projectScopedContainers, "number|string", functionNode); REQUIRE(functionNode.functionName == "MyExtension::ToString"); REQUIRE(type == "string"); REQUIRE(functionNode.objectName == ""); @@ -1809,7 +2163,7 @@ TEST_CASE("ExpressionParser2", "[common][events]") { REQUIRE(node != nullptr); auto &objectFunctionCall = dynamic_cast(*node); auto type = gd::ExpressionTypeFinder::GetType( - platform, objectsContainersList, "string", objectFunctionCall); + platform, projectScopedContainers, "string", objectFunctionCall); REQUIRE(objectFunctionCall.objectName == "MyObject"); REQUIRE(objectFunctionCall.functionName == ""); REQUIRE(type == "string"); @@ -1820,7 +2174,7 @@ TEST_CASE("ExpressionParser2", "[common][events]") { REQUIRE(node != nullptr); auto &objectFunctionCall = dynamic_cast(*node); auto type = gd::ExpressionTypeFinder::GetType( - platform, objectsContainersList, "number", objectFunctionCall); + platform, projectScopedContainers, "number", objectFunctionCall); REQUIRE(objectFunctionCall.objectName == "MyObject"); REQUIRE(objectFunctionCall.functionName == ""); REQUIRE(type == "number"); @@ -1833,7 +2187,7 @@ TEST_CASE("ExpressionParser2", "[common][events]") { REQUIRE(node != nullptr); auto &objectFunctionCall = dynamic_cast(*node); auto type = gd::ExpressionTypeFinder::GetType( - platform, objectsContainersList, "number|string", objectFunctionCall); + platform, projectScopedContainers, "number|string", objectFunctionCall); REQUIRE(objectFunctionCall.objectName == "MyObject"); REQUIRE(objectFunctionCall.functionName == ""); REQUIRE(type == "number|string"); @@ -1854,7 +2208,7 @@ TEST_CASE("ExpressionParser2", "[common][events]") { REQUIRE(node != nullptr); auto &objectFunctionName = dynamic_cast(*node); auto type = gd::ExpressionTypeFinder::GetType( - platform, objectsContainersList, "string", objectFunctionName); + platform, projectScopedContainers, "string", objectFunctionName); REQUIRE(objectFunctionName.objectName == "MyObject"); REQUIRE(objectFunctionName.behaviorName == "MyBehavior"); REQUIRE(objectFunctionName.functionName == ""); @@ -1866,7 +2220,7 @@ TEST_CASE("ExpressionParser2", "[common][events]") { REQUIRE(node != nullptr); auto &objectFunctionName = dynamic_cast(*node); auto type = gd::ExpressionTypeFinder::GetType( - platform, objectsContainersList, "number", objectFunctionName); + platform, projectScopedContainers, "number", objectFunctionName); REQUIRE(objectFunctionName.objectName == "MyObject"); REQUIRE(objectFunctionName.behaviorName == "MyBehavior"); REQUIRE(objectFunctionName.functionName == ""); @@ -1881,7 +2235,7 @@ TEST_CASE("ExpressionParser2", "[common][events]") { REQUIRE(node != nullptr); auto &objectFunctionName = dynamic_cast(*node); auto type = gd::ExpressionTypeFinder::GetType( - platform, objectsContainersList, "number|string", objectFunctionName); + platform, projectScopedContainers, "number|string", objectFunctionName); REQUIRE(objectFunctionName.objectName == "MyObject"); REQUIRE(objectFunctionName.behaviorName == "MyBehavior"); REQUIRE(objectFunctionName.functionName == ""); @@ -1893,7 +2247,7 @@ TEST_CASE("ExpressionParser2", "[common][events]") { REQUIRE(node != nullptr); auto &freeFunctionCall = dynamic_cast(*node); auto type = gd::ExpressionTypeFinder::GetType( - platform, objectsContainersList, "string", freeFunctionCall); + platform, projectScopedContainers, "string", freeFunctionCall); REQUIRE(freeFunctionCall.objectName == ""); REQUIRE(freeFunctionCall.functionName == "fun"); REQUIRE(type == "string"); @@ -1904,7 +2258,7 @@ TEST_CASE("ExpressionParser2", "[common][events]") { REQUIRE(node != nullptr); auto &freeFunctionCall = dynamic_cast(*node); auto type = gd::ExpressionTypeFinder::GetType( - platform, objectsContainersList, "number", freeFunctionCall); + platform, projectScopedContainers, "number", freeFunctionCall); REQUIRE(freeFunctionCall.objectName == ""); REQUIRE(freeFunctionCall.functionName == "fun"); REQUIRE(type == "number"); @@ -1916,7 +2270,7 @@ TEST_CASE("ExpressionParser2", "[common][events]") { REQUIRE(node != nullptr); auto &freeFunctionCall = dynamic_cast(*node); auto type = gd::ExpressionTypeFinder::GetType( - platform, objectsContainersList, "number|string", freeFunctionCall); + platform, projectScopedContainers, "number|string", freeFunctionCall); REQUIRE(freeFunctionCall.objectName == ""); REQUIRE(freeFunctionCall.functionName == "fun"); REQUIRE(type == "number|string"); @@ -2272,21 +2626,21 @@ TEST_CASE("ExpressionParser2", "[common][events]") { auto node = parser.ParseExpression("123"); REQUIRE(node != nullptr); auto type = gd::ExpressionTypeFinder::GetType( - platform, objectsContainersList, "number|string", *node.get()); + platform, projectScopedContainers, "number|string", *node.get()); REQUIRE(type == "number"); } { auto node = parser.ParseExpression("123 + MyExtension::GetNumber()"); REQUIRE(node != nullptr); auto type = gd::ExpressionTypeFinder::GetType( - platform, objectsContainersList, "number|string", *node.get()); + platform, projectScopedContainers, "number|string", *node.get()); REQUIRE(type == "number"); } { auto node = parser.ParseExpression("\"Hello\""); REQUIRE(node != nullptr); auto type = gd::ExpressionTypeFinder::GetType( - platform, objectsContainersList, "number|string", *node.get()); + platform, projectScopedContainers, "number|string", *node.get()); REQUIRE(type == "string"); } { @@ -2294,10 +2648,160 @@ TEST_CASE("ExpressionParser2", "[common][events]") { "\"Hello\" + MyExtension::ToString(3)"); REQUIRE(node != nullptr); auto type = gd::ExpressionTypeFinder::GetType( - platform, objectsContainersList, "number|string", *node.get()); + platform, projectScopedContainers, "number|string", *node.get()); REQUIRE(type == "string"); } } + SECTION("Valid type inferred from expressions with type 'number|string', with an known variable first") { + { + auto node = parser.ParseExpression("MySceneNumberVariable + 123"); + REQUIRE(node != nullptr); + auto type = gd::ExpressionTypeFinder::GetType( + platform, projectScopedContainers, "number|string", *node.get()); + REQUIRE(type == "number"); + } + { + auto node = parser.ParseExpression("MySceneStringVariable + \"hello world\""); + REQUIRE(node != nullptr); + auto type = gd::ExpressionTypeFinder::GetType( + platform, projectScopedContainers, "number|string", *node.get()); + REQUIRE(type == "string"); + } + { + auto node = parser.ParseExpression("MySceneNumberVariable + MySceneBooleanVariable + 123"); + REQUIRE(node != nullptr); + auto type = gd::ExpressionTypeFinder::GetType( + platform, projectScopedContainers, "number|string", *node.get()); + REQUIRE(type == "number"); + } + { + auto node = parser.ParseExpression("MySceneStringVariable + MySceneBooleanVariable + \"hello world\""); + REQUIRE(node != nullptr); + auto type = gd::ExpressionTypeFinder::GetType( + platform, projectScopedContainers, "number|string", *node.get()); + REQUIRE(type == "string"); + } + } + SECTION("Valid type inferred from expressions with type 'number|string', with an unknown variable first") { + { + auto node = parser.ParseExpression("MySceneBooleanVariable + 123"); + REQUIRE(node != nullptr); + auto type = gd::ExpressionTypeFinder::GetType( + platform, projectScopedContainers, "number|string", *node.get()); + REQUIRE(type == "number"); + } + { + auto node = parser.ParseExpression("MySceneBooleanVariable + \"hello world\""); + REQUIRE(node != nullptr); + auto type = gd::ExpressionTypeFinder::GetType( + platform, projectScopedContainers, "number|string", *node.get()); + REQUIRE(type == "string"); + } + { + auto node = parser.ParseExpression("MySceneStructureVariable.MyChild.UnknownSubChild + 123"); + REQUIRE(node != nullptr); + auto type = gd::ExpressionTypeFinder::GetType( + platform, projectScopedContainers, "number|string", *node.get()); + REQUIRE(type == "number"); + } + { + auto node = parser.ParseExpression("MySceneStructureVariable.MyChild.UnknownSubChild + \"hello world\""); + REQUIRE(node != nullptr); + auto type = gd::ExpressionTypeFinder::GetType( + platform, projectScopedContainers, "number|string", *node.get()); + REQUIRE(type == "string"); + } + } + SECTION("Valid type inferred from expressions with type 'number|string', with a property first") { + gd::PropertiesContainer propertiesContainer(gd::EventsFunctionsContainer::Extension); + + auto projectScopedContainersWithProperties = gd::ProjectScopedContainers::MakeNewProjectScopedContainersForProjectAndLayout(project, layout1); + projectScopedContainersWithProperties.AddPropertiesContainer(propertiesContainer); + + propertiesContainer.InsertNew("MyNumberProperty").SetType("Number"); + propertiesContainer.InsertNew("MyStringProperty").SetType("String"); + propertiesContainer.InsertNew("MyBooleanProperty").SetType("Boolean"); + { + auto node = parser.ParseExpression("MyNumberProperty + 123"); + REQUIRE(node != nullptr); + auto type = gd::ExpressionTypeFinder::GetType( + platform, projectScopedContainers, "number|string", *node.get()); + REQUIRE(type == "number"); + } + { + auto node = parser.ParseExpression("MyStringProperty + \"hello world\""); + REQUIRE(node != nullptr); + auto type = gd::ExpressionTypeFinder::GetType( + platform, projectScopedContainers, "number|string", *node.get()); + REQUIRE(type == "string"); + } + { + auto node = parser.ParseExpression("MyBooleanProperty + 123"); + REQUIRE(node != nullptr); + auto type = gd::ExpressionTypeFinder::GetType( + platform, projectScopedContainers, "number|string", *node.get()); + REQUIRE(type == "number"); + } + { + auto node = parser.ParseExpression("MyBooleanProperty + \"hello world\""); + REQUIRE(node != nullptr); + auto type = gd::ExpressionTypeFinder::GetType( + platform, projectScopedContainers, "number|string", *node.get()); + REQUIRE(type == "string"); + } + } + SECTION("Valid type inferred from expressions with type 'number|string', with a parameter first") { + std::vector parameters; + { + gd::ParameterMetadata param; + param.SetName("MyNumberParameter"); + param.SetType("number"); + parameters.push_back(param); + } + { + gd::ParameterMetadata param; + param.SetName("MyStringParameter"); + param.SetType("string"); + parameters.push_back(param); + } + { + gd::ParameterMetadata param; + param.SetName("MyBooleanParameter"); + param.SetType("yesorno"); + parameters.push_back(param); + } + + auto projectScopedContainersWithParameters = gd::ProjectScopedContainers::MakeNewProjectScopedContainersForProjectAndLayout(project, layout1); + projectScopedContainersWithParameters.AddParameters(parameters); + { + auto node = parser.ParseExpression("MyNumberParameter + 123"); + REQUIRE(node != nullptr); + auto type = gd::ExpressionTypeFinder::GetType( + platform, projectScopedContainersWithParameters, "number|string", *node.get()); + REQUIRE(type == "number"); + } + { + auto node = parser.ParseExpression("MyStringParameter + \"hello world\""); + REQUIRE(node != nullptr); + auto type = gd::ExpressionTypeFinder::GetType( + platform, projectScopedContainersWithParameters, "number|string", *node.get()); + REQUIRE(type == "string"); + } + { + auto node = parser.ParseExpression("MyBooleanParameter + \"hello world\""); + REQUIRE(node != nullptr); + auto type = gd::ExpressionTypeFinder::GetType( + platform, projectScopedContainersWithParameters, "number|string", *node.get()); + REQUIRE(type == "string"); + } + { + auto node = parser.ParseExpression("MyBooleanParameter + 123"); + REQUIRE(node != nullptr); + auto type = gd::ExpressionTypeFinder::GetType( + platform, projectScopedContainersWithParameters, "number|string", *node.get()); + REQUIRE(type == "number"); + } + } SECTION("Valid function call with object variable") { { diff --git a/GDJS/GDJS/Events/CodeGeneration/EventsCodeGenerator.cpp b/GDJS/GDJS/Events/CodeGeneration/EventsCodeGenerator.cpp index 96ef0b30c3..6386d8213c 100644 --- a/GDJS/GDJS/Events/CodeGeneration/EventsCodeGenerator.cpp +++ b/GDJS/GDJS/Events/CodeGeneration/EventsCodeGenerator.cpp @@ -1381,7 +1381,16 @@ gd::String EventsCodeGenerator::GeneratePropertyGetter( property.GetName())) + "()"; - if (type == "string") { + if (type == "number|string") { + if (property.GetType() == "Number") { + return propertyGetterCode; + } else if (property.GetType() == "Boolean") { + return "(" + propertyGetterCode + " ? \"true\" : \"false\")"; + } else { + // Assume type is String or equivalent. + return propertyGetterCode; + } + } else if (type == "string") { if (property.GetType() == "Number") { return "(\"\" + " + propertyGetterCode + ")"; } else if (property.GetType() == "Boolean") { @@ -1413,7 +1422,16 @@ gd::String EventsCodeGenerator::GenerateParameterGetter( gd::String parameterGetterCode = "eventsFunctionContext.getArgument(" + ConvertToStringExplicit(parameter.GetName()) + ")"; - if (type == "string") { + if (type == "number|string") { + if (parameter.GetValueTypeMetadata().IsNumber()) { + return parameterGetterCode; + } else if (parameter.GetValueTypeMetadata().IsBoolean()) { + return "(" + parameterGetterCode + " ? \"true\" : \"false\")"; + } else { + // Assume type is String or equivalent. + return parameterGetterCode; + } + } else if (type == "string") { if (parameter.GetValueTypeMetadata().IsNumber()) { return "(\"\" + " + parameterGetterCode + ")"; } else if (parameter.GetValueTypeMetadata().IsBoolean()) { diff --git a/GDJS/GDJS/Events/CodeGeneration/EventsCodeGenerator.h b/GDJS/GDJS/Events/CodeGeneration/EventsCodeGenerator.h index 699d8429be..b2371ef55e 100644 --- a/GDJS/GDJS/Events/CodeGeneration/EventsCodeGenerator.h +++ b/GDJS/GDJS/Events/CodeGeneration/EventsCodeGenerator.h @@ -302,11 +302,17 @@ class EventsCodeGenerator : public gd::EventsCodeGenerator { const gd::String& objectName); virtual gd::String GenerateVariableAccessor(gd::String childName) { + // This could be probably optimised by using `getChildNamed`. return ".getChild(" + ConvertToStringExplicit(childName) + ")"; }; virtual gd::String GenerateVariableBracketAccessor( gd::String expressionCode) { + // This uses `getChild` which allows to access a child + // with a number (an index, for an array) or a string (for a structure). + // This could be optimised, if the type of the accessed variable AND the type of the index is known, + // so that `getChildAt` (for an array, with an index) or `getChildNamed` (for a structure, with a name) + // is used instead. return ".getChild(" + expressionCode + ")"; }; diff --git a/GDJS/Runtime/variable.ts b/GDJS/Runtime/variable.ts index ce0b3b92c4..6cc84dda20 100644 --- a/GDJS/Runtime/variable.ts +++ b/GDJS/Runtime/variable.ts @@ -255,26 +255,43 @@ namespace gdjs { } } + /** + * Get the child with the specified name or at the specified index. + * + * If the variable is an array, prefer `getChildAt`. + * If the variable is a structure, prefer `getChildNamed`. + * + * If the variable has not the specified child, an empty variable with the specified name + * (or index) is added as child. + * + * @returns The child variable + */ + getChild(childName: string | number): gdjs.Variable { + if (this._type === 'array') + return this.getChildAt( + // @ts-ignore + Number.isInteger(childName) ? childName : parseInt(childName, 10) || 0 + ); + else { + if (this._type !== 'structure') this.castTo('structure'); + return this.getChildNamed('' + childName); + } + } + /** * Get the child with the specified name. * * If the variable has not the specified child, an empty variable with the specified name * is added as child. + * * @returns The child variable */ - getChild(childName: string): gdjs.Variable { - // Make sure the variable is a collection - if (this.isPrimitive()) this.castTo('structure'); + getChildNamed(childName: string): gdjs.Variable { + const child = this._children[childName]; + if (child === undefined || child === null) + return (this._children[childName] = new gdjs.Variable()); - if (this._type === 'array') - return this.getChildAt(parseInt(childName, 10) || 0); - - if ( - this._children[childName] === undefined || - this._children[childName] === null - ) - this._children[childName] = new gdjs.Variable(); - return this._children[childName]; + return child; } /** @@ -286,8 +303,8 @@ namespace gdjs { * @returns The variable (for chaining calls) */ addChild(childName: string, childVariable: gdjs.Variable): this { - // Make sure this is a structure - this.castTo('structure'); + if (this._type !== 'structure') this.castTo('structure'); + this._children[childName] = childVariable; return this; } @@ -393,6 +410,21 @@ namespace gdjs { this._str = '' + newValue; } + /** + * Get the value of the variable, as a number if it's one, + * or as a string (if it's a string or something else) + * + * In most cases, prefer calling `getAsNumber` or `getAsString` directly. + * This is a fallback in case a variable type can't be known statically for sure, + * like `getValue`. + * + * @private + */ + getAsNumberOrString(): number | string { + if (this._type === 'number') return this._value; + return this.getAsString(); + } + /** * Get the value of the variable, considered as a boolean * @return The boolean value of the variable. @@ -550,7 +582,8 @@ namespace gdjs { * Get a variable at a given index of the array. */ getChildAt(index: integer) { - this.castTo('array'); + if (this._type !== 'array') this.castTo('array'); + if ( this._childrenArray[index] === undefined || this._childrenArray[index] === null @@ -570,7 +603,8 @@ namespace gdjs { * Pushes a copy of a variable into the array. */ pushVariableCopy(variable: gdjs.Variable) { - this.castTo('array'); + if (this._type !== 'array') this.castTo('array'); + this._childrenArray.push(variable.clone()); } @@ -578,7 +612,8 @@ namespace gdjs { * Pushes a value into the array. */ pushValue(value: string | float | boolean) { - this.castTo('array'); + if (this._type !== 'array') this.castTo('array'); + this._childrenArray.push( new gdjs.Variable({ type: typeof value as 'string' | 'number' | 'boolean', diff --git a/GDJS/Runtime/variablescontainer.ts b/GDJS/Runtime/variablescontainer.ts index 6bd8df9c35..302c78cc5d 100644 --- a/GDJS/Runtime/variablescontainer.ts +++ b/GDJS/Runtime/variablescontainer.ts @@ -244,6 +244,7 @@ namespace gdjs { getValue: () => 0, getChild: () => gdjs.VariablesContainer.badVariable, getChildAt: () => gdjs.VariablesContainer.badVariable, + getChildNamed: () => gdjs.VariablesContainer.badVariable, hasChild: function () { return false; }, @@ -271,6 +272,9 @@ namespace gdjs { getAsNumber: function () { return 0; }, + getAsNumberOrString: function () { + return 0; + }, getAsBoolean: function () { return false; }, diff --git a/GDJS/tests/games/structure-variables-foreach/assets/ArchitectsDaughter.ttf b/GDJS/tests/games/structure-variables-foreach/assets/ArchitectsDaughter.ttf new file mode 100644 index 0000000000..f4469b7013 Binary files /dev/null and b/GDJS/tests/games/structure-variables-foreach/assets/ArchitectsDaughter.ttf differ diff --git a/GDJS/tests/games/structure-variables-foreach/assets/Black Decorated Button_Hovered.png b/GDJS/tests/games/structure-variables-foreach/assets/Black Decorated Button_Hovered.png new file mode 100644 index 0000000000..c07f8106fe Binary files /dev/null and b/GDJS/tests/games/structure-variables-foreach/assets/Black Decorated Button_Hovered.png differ diff --git a/GDJS/tests/games/structure-variables-foreach/assets/Black Decorated Button_Idle.png b/GDJS/tests/games/structure-variables-foreach/assets/Black Decorated Button_Idle.png new file mode 100644 index 0000000000..7bca4b2e66 Binary files /dev/null and b/GDJS/tests/games/structure-variables-foreach/assets/Black Decorated Button_Idle.png differ diff --git a/GDJS/tests/games/structure-variables-foreach/assets/Black Decorated Button_Pressed.png b/GDJS/tests/games/structure-variables-foreach/assets/Black Decorated Button_Pressed.png new file mode 100644 index 0000000000..b22d072e4a Binary files /dev/null and b/GDJS/tests/games/structure-variables-foreach/assets/Black Decorated Button_Pressed.png differ diff --git a/GDJS/tests/games/structure-variables-foreach/structure-variables-foreach.json b/GDJS/tests/games/structure-variables-foreach/structure-variables-foreach.json new file mode 100644 index 0000000000..49ce85485d --- /dev/null +++ b/GDJS/tests/games/structure-variables-foreach/structure-variables-foreach.json @@ -0,0 +1,3884 @@ +{ + "firstLayout": "", + "gdVersion": { + "build": 99, + "major": 4, + "minor": 0, + "revision": 0 + }, + "properties": { + "adaptGameResolutionAtRuntime": true, + "antialiasingMode": "MSAA", + "antialisingEnabledOnMobile": false, + "folderProject": false, + "orientation": "landscape", + "packageName": "com.example.gamename", + "pixelsRounding": false, + "projectUuid": "af28e5c5-9391-40eb-bc17-397f6e7ecf70", + "scaleMode": "linear", + "sizeOnStartupMode": "adaptWidth", + "templateSlug": "", + "useExternalSourceFiles": false, + "version": "1.0.0", + "name": "structure test", + "description": "", + "author": "", + "windowWidth": 1280, + "windowHeight": 720, + "latestCompilationDirectory": "", + "maxFPS": 60, + "minFPS": 20, + "verticalSync": false, + "platformSpecificAssets": {}, + "loadingScreen": { + "backgroundColor": 0, + "backgroundFadeInDuration": 0.2, + "backgroundImageResourceName": "", + "gdevelopLogoStyle": "light", + "logoAndProgressFadeInDuration": 0.2, + "logoAndProgressLogoFadeInDelay": 0.2, + "minDuration": 1.5, + "progressBarColor": 16777215, + "progressBarHeight": 20, + "progressBarMaxWidth": 200, + "progressBarMinWidth": 40, + "progressBarWidthPercent": 30, + "showGDevelopSplash": true, + "showProgressBar": true + }, + "watermark": { + "placement": "bottom-left", + "showWatermark": true + }, + "authorIds": [], + "authorUsernames": [], + "categories": [], + "playableDevices": [], + "extensionProperties": [], + "platforms": [ + { + "name": "GDevelop JS platform" + } + ], + "currentPlatform": "GDevelop JS platform" + }, + "resources": { + "resources": [ + { + "alwaysLoaded": false, + "file": "assets/Black Decorated Button_Hovered.png", + "kind": "image", + "metadata": "", + "name": "Black Decorated Button_Hovered.png", + "smoothed": true, + "userAdded": false, + "origin": { + "identifier": "https://asset-resources.gdevelop.io/public-resources/Menu buttons/8a9f8afaf0442f49cd7d5318d31ac6117137ec91d9086e9bdd59b052a4fa71f0_Black Decorated Button_Hovered.png", + "name": "Black Decorated Button_Hovered.png" + } + }, + { + "alwaysLoaded": false, + "file": "assets/Black Decorated Button_Idle.png", + "kind": "image", + "metadata": "", + "name": "Black Decorated Button_Idle.png", + "smoothed": true, + "userAdded": false, + "origin": { + "identifier": "https://asset-resources.gdevelop.io/public-resources/Menu buttons/6652cc81d33cb5485e0bf19ed2a92aac70b5c9fb45837762d094c8dbecc30d4d_Black Decorated Button_Idle.png", + "name": "Black Decorated Button_Idle.png" + } + }, + { + "alwaysLoaded": false, + "file": "assets/Black Decorated Button_Pressed.png", + "kind": "image", + "metadata": "", + "name": "Black Decorated Button_Pressed.png", + "smoothed": true, + "userAdded": false, + "origin": { + "identifier": "https://asset-resources.gdevelop.io/public-resources/Menu buttons/61997299d1d329fff69772b1111ba7c07b6c0a96a9a17090d2bf2cc31ff08e00_Black Decorated Button_Pressed.png", + "name": "Black Decorated Button_Pressed.png" + } + }, + { + "file": "assets/ArchitectsDaughter.ttf", + "kind": "font", + "metadata": "", + "name": "ArchitectsDaughter.ttf", + "userAdded": false, + "origin": { + "identifier": "https://asset-resources.gdevelop.io/public-resources/Menu buttons/ab812c7d15ad3474b6e55761235713b68f34374f3f064d1b8685ed46720df2f8_ArchitectsDaughter.ttf", + "name": "ArchitectsDaughter.ttf" + } + } + ], + "resourceFolders": [] + }, + "objects": [], + "objectsGroups": [], + "variables": [], + "layouts": [ + { + "b": 209, + "disableInputWhenNotFocused": true, + "mangledName": "Untitled_32scene", + "name": "Untitled scene", + "r": 209, + "standardSortMethod": true, + "stopSoundsOnStartup": true, + "title": "", + "v": 209, + "uiSettings": { + "grid": false, + "gridType": "rectangular", + "gridWidth": 32, + "gridHeight": 32, + "gridOffsetX": 0, + "gridOffsetY": 0, + "gridColor": 10401023, + "gridAlpha": 0.8, + "snap": false, + "zoomFactor": 0.546875, + "windowMask": false + }, + "objectsGroups": [], + "variables": [ + { + "name": "LabelText", + "type": "string", + "value": "" + }, + { + "folded": true, + "name": "SceneStructure", + "type": "structure", + "children": [] + } + ], + "instances": [ + { + "angle": 0, + "customSize": false, + "height": 0, + "layer": "", + "name": "NewText", + "persistentUuid": "7827b8b3-3768-40a8-8d9a-abb7de3dfb90", + "width": 0, + "x": 526, + "y": 112, + "zOrder": 1, + "numberProperties": [], + "stringProperties": [], + "initialVariables": [] + }, + { + "angle": 0, + "customSize": false, + "height": 0, + "layer": "", + "name": "Button1", + "persistentUuid": "335c7d45-7bfb-46d3-8f5a-bcc520718319", + "width": 0, + "x": 25, + "y": 313, + "zOrder": 2, + "numberProperties": [], + "stringProperties": [], + "initialVariables": [] + }, + { + "angle": 0, + "customSize": false, + "height": 0, + "layer": "", + "name": "Button2", + "persistentUuid": "5155e533-26c4-4de0-8b50-ad89d63784fb", + "width": 0, + "x": 251, + "y": 318, + "zOrder": 3, + "numberProperties": [], + "stringProperties": [], + "initialVariables": [] + }, + { + "angle": 0, + "customSize": false, + "height": 0, + "layer": "", + "name": "Button3", + "persistentUuid": "ad753d63-c367-444b-8946-a8f3915467bd", + "width": 0, + "x": 476, + "y": 311, + "zOrder": 4, + "numberProperties": [], + "stringProperties": [], + "initialVariables": [] + }, + { + "angle": 0, + "customSize": false, + "height": 0, + "layer": "", + "name": "Button4", + "persistentUuid": "0f5eaa16-b936-4dd9-85f5-a624e3cf20f0", + "width": 0, + "x": 697, + "y": 316, + "zOrder": 5, + "numberProperties": [], + "stringProperties": [], + "initialVariables": [] + }, + { + "angle": 0, + "customSize": false, + "height": 0, + "layer": "", + "name": "Button5", + "persistentUuid": "a1ad8348-f8bd-4f45-8021-cb3c478aaa03", + "width": 0, + "x": 931, + "y": 316, + "zOrder": 6, + "numberProperties": [], + "stringProperties": [], + "initialVariables": [] + }, + { + "angle": 0, + "customSize": false, + "height": 0, + "layer": "", + "name": "Button6", + "persistentUuid": "ff953303-9a3d-431f-a131-ca50e8e33205", + "width": 0, + "x": 1038, + "y": 409, + "zOrder": 7, + "numberProperties": [], + "stringProperties": [], + "initialVariables": [] + } + ], + "objects": [ + { + "assetStoreId": "5f0df6131c9646dcbe98798196db5b6d806eb25564f86d2e119a084d1e20bf74", + "name": "Button1", + "tags": "", + "type": "PanelSpriteButton::PanelSpriteButton", + "variables": [], + "effects": [], + "behaviors": [], + "content": { + "LeftPadding": 16, + "RightPadding": 16, + "PressedLabelOffsetY": 6, + "BottomPadding": 24, + "TopPadding": 20, + "HoveredFadeOutDuration": 0.25 + }, + "childrenContent": { + "Hovered": { + "bottomMargin": 22, + "height": 64, + "leftMargin": 16, + "rightMargin": 16, + "texture": "Black Decorated Button_Hovered.png", + "tiled": true, + "topMargin": 16, + "width": 192 + }, + "Idle": { + "bottomMargin": 22, + "height": 64, + "leftMargin": 16, + "rightMargin": 16, + "texture": "Black Decorated Button_Idle.png", + "tiled": true, + "topMargin": 16, + "width": 192 + }, + "Label": { + "bold": false, + "italic": false, + "smoothed": true, + "underlined": false, + "string": "One", + "font": "ArchitectsDaughter.ttf", + "textAlignment": "center", + "characterSize": 22, + "color": { + "b": 255, + "g": 255, + "r": 255 + } + }, + "Pressed": { + "bottomMargin": 16, + "height": 64, + "leftMargin": 16, + "rightMargin": 16, + "texture": "Black Decorated Button_Pressed.png", + "tiled": true, + "topMargin": 22, + "width": 192 + } + } + }, + { + "assetStoreId": "5f0df6131c9646dcbe98798196db5b6d806eb25564f86d2e119a084d1e20bf74", + "name": "Button2", + "tags": "", + "type": "PanelSpriteButton::PanelSpriteButton", + "variables": [], + "effects": [], + "behaviors": [], + "content": { + "LeftPadding": 16, + "RightPadding": 16, + "PressedLabelOffsetY": 6, + "BottomPadding": 24, + "TopPadding": 20, + "HoveredFadeOutDuration": 0.25 + }, + "childrenContent": { + "Hovered": { + "bottomMargin": 22, + "height": 64, + "leftMargin": 16, + "rightMargin": 16, + "texture": "Black Decorated Button_Hovered.png", + "tiled": true, + "topMargin": 16, + "width": 192 + }, + "Idle": { + "bottomMargin": 22, + "height": 64, + "leftMargin": 16, + "rightMargin": 16, + "texture": "Black Decorated Button_Idle.png", + "tiled": true, + "topMargin": 16, + "width": 192 + }, + "Label": { + "bold": false, + "italic": false, + "smoothed": true, + "underlined": false, + "string": "One", + "font": "ArchitectsDaughter.ttf", + "textAlignment": "center", + "characterSize": 22, + "color": { + "b": 255, + "g": 255, + "r": 255 + } + }, + "Pressed": { + "bottomMargin": 16, + "height": 64, + "leftMargin": 16, + "rightMargin": 16, + "texture": "Black Decorated Button_Pressed.png", + "tiled": true, + "topMargin": 22, + "width": 192 + } + } + }, + { + "assetStoreId": "5f0df6131c9646dcbe98798196db5b6d806eb25564f86d2e119a084d1e20bf74", + "name": "Button3", + "tags": "", + "type": "PanelSpriteButton::PanelSpriteButton", + "variables": [], + "effects": [], + "behaviors": [], + "content": { + "LeftPadding": 16, + "RightPadding": 16, + "PressedLabelOffsetY": 6, + "BottomPadding": 24, + "TopPadding": 20, + "HoveredFadeOutDuration": 0.25 + }, + "childrenContent": { + "Hovered": { + "bottomMargin": 22, + "height": 64, + "leftMargin": 16, + "rightMargin": 16, + "texture": "Black Decorated Button_Hovered.png", + "tiled": true, + "topMargin": 16, + "width": 192 + }, + "Idle": { + "bottomMargin": 22, + "height": 64, + "leftMargin": 16, + "rightMargin": 16, + "texture": "Black Decorated Button_Idle.png", + "tiled": true, + "topMargin": 16, + "width": 192 + }, + "Label": { + "bold": false, + "italic": false, + "smoothed": true, + "underlined": false, + "string": "One", + "font": "ArchitectsDaughter.ttf", + "textAlignment": "center", + "characterSize": 22, + "color": { + "b": 255, + "g": 255, + "r": 255 + } + }, + "Pressed": { + "bottomMargin": 16, + "height": 64, + "leftMargin": 16, + "rightMargin": 16, + "texture": "Black Decorated Button_Pressed.png", + "tiled": true, + "topMargin": 22, + "width": 192 + } + } + }, + { + "assetStoreId": "5f0df6131c9646dcbe98798196db5b6d806eb25564f86d2e119a084d1e20bf74", + "name": "Button4", + "tags": "", + "type": "PanelSpriteButton::PanelSpriteButton", + "variables": [], + "effects": [], + "behaviors": [], + "content": { + "LeftPadding": 16, + "RightPadding": 16, + "PressedLabelOffsetY": 6, + "BottomPadding": 24, + "TopPadding": 20, + "HoveredFadeOutDuration": 0.25 + }, + "childrenContent": { + "Hovered": { + "bottomMargin": 22, + "height": 64, + "leftMargin": 16, + "rightMargin": 16, + "texture": "Black Decorated Button_Hovered.png", + "tiled": true, + "topMargin": 16, + "width": 192 + }, + "Idle": { + "bottomMargin": 22, + "height": 64, + "leftMargin": 16, + "rightMargin": 16, + "texture": "Black Decorated Button_Idle.png", + "tiled": true, + "topMargin": 16, + "width": 192 + }, + "Label": { + "bold": false, + "italic": false, + "smoothed": true, + "underlined": false, + "string": "One", + "font": "ArchitectsDaughter.ttf", + "textAlignment": "center", + "characterSize": 22, + "color": { + "b": 255, + "g": 255, + "r": 255 + } + }, + "Pressed": { + "bottomMargin": 16, + "height": 64, + "leftMargin": 16, + "rightMargin": 16, + "texture": "Black Decorated Button_Pressed.png", + "tiled": true, + "topMargin": 22, + "width": 192 + } + } + }, + { + "assetStoreId": "5f0df6131c9646dcbe98798196db5b6d806eb25564f86d2e119a084d1e20bf74", + "name": "Button5", + "tags": "", + "type": "PanelSpriteButton::PanelSpriteButton", + "variables": [], + "effects": [], + "behaviors": [], + "content": { + "LeftPadding": 16, + "RightPadding": 16, + "PressedLabelOffsetY": 6, + "BottomPadding": 24, + "TopPadding": 20, + "HoveredFadeOutDuration": 0.25 + }, + "childrenContent": { + "Hovered": { + "bottomMargin": 22, + "height": 64, + "leftMargin": 16, + "rightMargin": 16, + "texture": "Black Decorated Button_Hovered.png", + "tiled": true, + "topMargin": 16, + "width": 192 + }, + "Idle": { + "bottomMargin": 22, + "height": 64, + "leftMargin": 16, + "rightMargin": 16, + "texture": "Black Decorated Button_Idle.png", + "tiled": true, + "topMargin": 16, + "width": 192 + }, + "Label": { + "bold": false, + "italic": false, + "smoothed": true, + "underlined": false, + "string": "One", + "font": "ArchitectsDaughter.ttf", + "textAlignment": "center", + "characterSize": 22, + "color": { + "b": 255, + "g": 255, + "r": 255 + } + }, + "Pressed": { + "bottomMargin": 16, + "height": 64, + "leftMargin": 16, + "rightMargin": 16, + "texture": "Black Decorated Button_Pressed.png", + "tiled": true, + "topMargin": 22, + "width": 192 + } + } + }, + { + "assetStoreId": "5f0df6131c9646dcbe98798196db5b6d806eb25564f86d2e119a084d1e20bf74", + "name": "Button6", + "tags": "", + "type": "PanelSpriteButton::PanelSpriteButton", + "variables": [], + "effects": [], + "behaviors": [], + "content": { + "LeftPadding": 16, + "RightPadding": 16, + "PressedLabelOffsetY": 6, + "BottomPadding": 24, + "TopPadding": 20, + "HoveredFadeOutDuration": 0.25 + }, + "childrenContent": { + "Hovered": { + "bottomMargin": 22, + "height": 64, + "leftMargin": 16, + "rightMargin": 16, + "texture": "Black Decorated Button_Hovered.png", + "tiled": true, + "topMargin": 16, + "width": 192 + }, + "Idle": { + "bottomMargin": 22, + "height": 64, + "leftMargin": 16, + "rightMargin": 16, + "texture": "Black Decorated Button_Idle.png", + "tiled": true, + "topMargin": 16, + "width": 192 + }, + "Label": { + "bold": false, + "italic": false, + "smoothed": true, + "underlined": false, + "string": "One", + "font": "ArchitectsDaughter.ttf", + "textAlignment": "center", + "characterSize": 22, + "color": { + "b": 255, + "g": 255, + "r": 255 + } + }, + "Pressed": { + "bottomMargin": 16, + "height": 64, + "leftMargin": 16, + "rightMargin": 16, + "texture": "Black Decorated Button_Pressed.png", + "tiled": true, + "topMargin": 22, + "width": 192 + } + } + }, + { + "assetStoreId": "", + "bold": false, + "italic": false, + "name": "NewText", + "smoothed": true, + "tags": "", + "type": "TextObject::Text", + "underlined": false, + "variables": [], + "effects": [], + "behaviors": [], + "string": "Text", + "font": "", + "textAlignment": "left", + "characterSize": 20, + "color": { + "b": 0, + "g": 0, + "r": 0 + } + } + ], + "events": [ + { + "colorB": 228, + "colorG": 176, + "colorR": 74, + "creationTime": 0, + "name": "Create a simple structure. No variables are declared in the Variable screen", + "source": "", + "type": "BuiltinCommonInstructions::Group", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "DepartScene" + }, + "parameters": [ + "" + ] + } + ], + "actions": [ + { + "type": { + "value": "ModVarSceneTxt" + }, + "parameters": [ + "Player.One.Name", + "=", + "\"Smith\"" + ] + }, + { + "type": { + "value": "ModVarScene" + }, + "parameters": [ + "Player.One.Age", + "=", + "25" + ] + }, + { + "type": { + "value": "ModVarSceneTxt" + }, + "parameters": [ + "Player.One.County", + "=", + "\"France\"" + ] + } + ] + } + ], + "parameters": [] + }, + { + "colorB": 228, + "colorG": 176, + "colorR": 74, + "creationTime": 0, + "name": "Works", + "source": "", + "type": "BuiltinCommonInstructions::Group", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "PanelSpriteButton::PanelSpriteButton::IsClicked" + }, + "parameters": [ + "Button1", + "" + ] + } + ], + "actions": [ + { + "type": { + "value": "TextObject::String" + }, + "parameters": [ + "NewText", + "=", + "\"1\" + NewLine()" + ] + } + ], + "events": [ + { + "type": "BuiltinCommonInstructions::ForEachChildVariable", + "iterableVariableName": "Player.One", + "valueIteratorVariableName": "child", + "keyIteratorVariableName": "childName", + "conditions": [], + "actions": [ + { + "type": { + "value": "TextObject::String" + }, + "parameters": [ + "NewText", + "+", + "VariableString(childName) + \": \" + \nVariableString(child) + NewLine()" + ] + } + ] + } + ] + } + ], + "parameters": [] + }, + { + "colorB": 228, + "colorG": 176, + "colorR": 74, + "creationTime": 0, + "name": "Works", + "source": "", + "type": "BuiltinCommonInstructions::Group", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "PanelSpriteButton::PanelSpriteButton::IsClicked" + }, + "parameters": [ + "Button2", + "" + ] + } + ], + "actions": [ + { + "type": { + "value": "TextObject::String" + }, + "parameters": [ + "NewText", + "=", + "\"2\" + NewLine()" + ] + } + ], + "events": [ + { + "type": "BuiltinCommonInstructions::ForEachChildVariable", + "iterableVariableName": "Player[\"One\"]", + "valueIteratorVariableName": "child", + "keyIteratorVariableName": "childName", + "conditions": [], + "actions": [ + { + "type": { + "value": "TextObject::String" + }, + "parameters": [ + "NewText", + "+", + "VariableString(childName) + \": \" + \nVariableString(child) + NewLine()" + ] + } + ] + } + ] + } + ], + "parameters": [] + }, + { + "colorB": 228, + "colorG": 176, + "colorR": 74, + "creationTime": 0, + "name": "Doesn't Work and creates a child under Player without a name just \"\" - NEVER worked (even in 5.2.172)", + "source": "", + "type": "BuiltinCommonInstructions::Group", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "PanelSpriteButton::PanelSpriteButton::IsClicked" + }, + "parameters": [ + "Button3", + "" + ] + } + ], + "actions": [ + { + "type": { + "value": "TextObject::String" + }, + "parameters": [ + "NewText", + "=", + "\"3\" + NewLine()" + ] + } + ], + "events": [ + { + "type": "BuiltinCommonInstructions::ForEachChildVariable", + "iterableVariableName": "Player[Button3.LabelText()]", + "valueIteratorVariableName": "child", + "keyIteratorVariableName": "childName", + "conditions": [], + "actions": [ + { + "type": { + "value": "TextObject::String" + }, + "parameters": [ + "NewText", + "+", + "VariableString(childName) + \": \" + \nVariableString(child) + NewLine()" + ] + } + ] + } + ] + } + ], + "parameters": [] + }, + { + "colorB": 228, + "colorG": 176, + "colorR": 74, + "creationTime": 0, + "name": "Works - setting the label text to a variable", + "source": "", + "type": "BuiltinCommonInstructions::Group", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "PanelSpriteButton::PanelSpriteButton::IsClicked" + }, + "parameters": [ + "Button4", + "" + ] + } + ], + "actions": [ + { + "type": { + "value": "TextObject::String" + }, + "parameters": [ + "NewText", + "=", + "\"4\" + NewLine()" + ] + }, + { + "type": { + "value": "ModVarSceneTxt" + }, + "parameters": [ + "LabelText", + "=", + "Button3.LabelText()" + ] + } + ], + "events": [ + { + "type": "BuiltinCommonInstructions::ForEachChildVariable", + "iterableVariableName": "Player[VariableString(LabelText)]", + "valueIteratorVariableName": "child", + "keyIteratorVariableName": "childName", + "conditions": [], + "actions": [ + { + "type": { + "value": "TextObject::String" + }, + "parameters": [ + "NewText", + "+", + "VariableString(childName) + \": \" + \nVariableString(child) + NewLine()" + ] + } + ] + } + ] + } + ], + "parameters": [] + }, + { + "colorB": 228, + "colorG": 176, + "colorR": 74, + "creationTime": 0, + "name": "New syntax - needs LabelText to be declared as a string (will be FIXED in 5.2.176)", + "source": "", + "type": "BuiltinCommonInstructions::Group", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "PanelSpriteButton::PanelSpriteButton::IsClicked" + }, + "parameters": [ + "Button5", + "" + ] + } + ], + "actions": [ + { + "type": { + "value": "TextObject::String" + }, + "parameters": [ + "NewText", + "=", + "\"5\" + NewLine()" + ] + }, + { + "type": { + "value": "ModVarSceneTxt" + }, + "parameters": [ + "LabelText", + "=", + "Button3.LabelText()" + ] + } + ], + "events": [ + { + "type": "BuiltinCommonInstructions::ForEachChildVariable", + "iterableVariableName": "Player[LabelText]", + "valueIteratorVariableName": "child", + "keyIteratorVariableName": "childName", + "conditions": [], + "actions": [ + { + "type": { + "value": "TextObject::String" + }, + "parameters": [ + "NewText", + "+", + "VariableString(childName) + \": \" + \nVariableString(child) + NewLine()" + ] + } + ] + } + ] + } + ], + "parameters": [] + }, + { + "colorB": 228, + "colorG": 176, + "colorR": 74, + "creationTime": 0, + "name": "New syntax - needs SceneStructure to be declared as a structure (will be FIXED in 5.2.176)", + "source": "", + "type": "BuiltinCommonInstructions::Group", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "PanelSpriteButton::PanelSpriteButton::IsClicked" + }, + "parameters": [ + "Button6", + "" + ] + } + ], + "actions": [ + { + "type": { + "value": "TextObject::String" + }, + "parameters": [ + "NewText", + "=", + "\"6\" + NewLine()" + ] + }, + { + "type": { + "value": "ModVarSceneTxt" + }, + "parameters": [ + "SceneStructure.child.index", + "=", + "Button3.LabelText()" + ] + } + ], + "events": [ + { + "type": "BuiltinCommonInstructions::ForEachChildVariable", + "iterableVariableName": "Player[SceneStructure.child.index]", + "valueIteratorVariableName": "child", + "keyIteratorVariableName": "childName", + "conditions": [], + "actions": [ + { + "type": { + "value": "TextObject::String" + }, + "parameters": [ + "NewText", + "+", + "VariableString(childName) + \": \" + \nVariableString(child) + NewLine()" + ] + } + ] + } + ] + } + ], + "parameters": [] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [] + } + ], + "layers": [ + { + "ambientLightColorB": 200, + "ambientLightColorG": 200, + "ambientLightColorR": 200, + "camera3DFarPlaneDistance": 10000, + "camera3DFieldOfView": 45, + "camera3DNearPlaneDistance": 0.1, + "followBaseLayerCamera": false, + "isLightingLayer": false, + "isLocked": false, + "name": "", + "renderingType": "", + "visibility": true, + "cameras": [ + { + "defaultSize": true, + "defaultViewport": true, + "height": 0, + "viewportBottom": 1, + "viewportLeft": 0, + "viewportRight": 1, + "viewportTop": 0, + "width": 0 + } + ], + "effects": [ + { + "effectType": "Scene3D::HemisphereLight", + "name": "3D Light", + "doubleParameters": { + "elevation": 45, + "intensity": 1, + "rotation": 0 + }, + "stringParameters": { + "groundColor": "64;64;64", + "skyColor": "255;255;255", + "top": "Y-" + }, + "booleanParameters": {} + } + ] + } + ], + "behaviorsSharedData": [ + { + "name": "Effect", + "type": "EffectCapability::EffectBehavior" + }, + { + "name": "Flippable", + "type": "FlippableCapability::FlippableBehavior" + }, + { + "name": "Opacity", + "type": "OpacityCapability::OpacityBehavior" + }, + { + "name": "Resizable", + "type": "ResizableCapability::ResizableBehavior" + }, + { + "name": "Scale", + "type": "ScalableCapability::ScalableBehavior" + } + ] + } + ], + "externalEvents": [], + "eventsFunctionsExtensions": [ + { + "author": "", + "category": "User interface", + "extensionNamespace": "", + "fullName": "Panel sprite button", + "helpPath": "/objects/button", + "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAyMy4wLjMsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjxzdmcgdmVyc2lvbj0iMS4xIiBpZD0iSWNvbnMiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB2aWV3Qm94PSIwIDAgMzIgMzIiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDMyIDMyOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+DQo8c3R5bGUgdHlwZT0idGV4dC9jc3MiPg0KCS5zdDB7ZmlsbDpub25lO3N0cm9rZTojMDAwMDAwO3N0cm9rZS13aWR0aDoyO3N0cm9rZS1saW5lY2FwOnJvdW5kO3N0cm9rZS1saW5lam9pbjpyb3VuZDtzdHJva2UtbWl0ZXJsaW1pdDoxMDt9DQo8L3N0eWxlPg0KPHBhdGggY2xhc3M9InN0MCIgZD0iTTI5LDIzSDNjLTEuMSwwLTItMC45LTItMlYxMWMwLTEuMSwwLjktMiwyLTJoMjZjMS4xLDAsMiwwLjksMiwydjEwQzMxLDIyLjEsMzAuMSwyMywyOSwyM3oiLz4NCjxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik0xMywxOUwxMywxOWMtMS4xLDAtMi0wLjktMi0ydi0yYzAtMS4xLDAuOS0yLDItMmgwYzEuMSwwLDIsMC45LDIsMnYyQzE1LDE4LjEsMTQuMSwxOSwxMywxOXoiLz4NCjxsaW5lIGNsYXNzPSJzdDAiIHgxPSIxOCIgeTE9IjEzIiB4Mj0iMTgiIHkyPSIxOSIvPg0KPGxpbmUgY2xhc3M9InN0MCIgeDE9IjIxIiB5MT0iMTMiIHgyPSIxOCIgeTI9IjE3Ii8+DQo8bGluZSBjbGFzcz0ic3QwIiB4MT0iMjEiIHkxPSIxOSIgeDI9IjE5IiB5Mj0iMTYiLz4NCjwvc3ZnPg0K", + "name": "PanelSpriteButton", + "previewIconUrl": "https://resources.gdevelop-app.com/assets/Icons/Line Hero Pack/Master/SVG/Interface Elements/Interface Elements_interface_ui_button_ok_cta_clock_tap.svg", + "shortDescription": "A button that can be customized.", + "version": "1.4.4", + "description": [ + "The button can be customized with a background for each state and a label. It handles user interactions and a simple condition can be used to check if it is clicked.", + "", + "There are ready-to-use buttons in the asset-store [menu buttons pack](https://editor.gdevelop.io/?initial-dialog=asset-store&asset-pack=menu-buttons-menu-buttons)." + ], + "origin": { + "identifier": "PanelSpriteButton", + "name": "gdevelop-extension-store" + }, + "tags": [ + "button", + "ui" + ], + "authorIds": [ + "IWykYNRvhCZBN3vEgKEbBPOR3Oc2" + ], + "dependencies": [], + "eventsFunctions": [], + "eventsBasedBehaviors": [ + { + "description": "The finite state machine used internally by the button object.", + "fullName": "Button finite state machine", + "name": "ButtonFSM", + "objectType": "", + "private": true, + "eventsFunctions": [ + { + "fullName": "", + "functionType": "Action", + "name": "doStepPostEvents", + "sentence": "", + "events": [ + { + "colorB": 228, + "colorG": 176, + "colorR": 74, + "creationTime": 0, + "name": "Finite state machine", + "source": "", + "type": "BuiltinCommonInstructions::Group", + "events": [ + { + "type": "BuiltinCommonInstructions::Comment", + "color": { + "b": 109, + "g": 230, + "r": 255, + "textB": 0, + "textG": 0, + "textR": 0 + }, + "comment": "The \"Validated\" state only last one frame." + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "PanelSpriteButton::ButtonFSM::PropertyState" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "\"Validated\"" + ] + } + ], + "actions": [ + { + "type": { + "value": "PanelSpriteButton::ButtonFSM::SetPropertyState" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "\"Idle\"" + ] + } + ] + }, + { + "colorB": 228, + "colorG": 176, + "colorR": 74, + "creationTime": 0, + "name": "Check position", + "source": "", + "type": "BuiltinCommonInstructions::Group", + "events": [ + { + "type": "BuiltinCommonInstructions::Comment", + "color": { + "b": 109, + "g": 230, + "r": 255, + "textB": 0, + "textG": 0, + "textR": 0 + }, + "comment": "Make sure the cursor position is only checked once per frame." + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "PanelSpriteButton::ButtonFSM::SetPropertyMouseIsInside" + }, + "parameters": [ + "Object", + "Behavior", + "no" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "PanelSpriteButton::ButtonFSM::PropertyShouldCheckHovering" + }, + "parameters": [ + "Object", + "Behavior" + ] + }, + { + "type": { + "value": "CollisionPoint" + }, + "parameters": [ + "Object", + "MouseOnlyCursorX(Object.Layer(), 0)", + "MouseOnlyCursorY(Object.Layer(), 0)" + ] + } + ], + "actions": [ + { + "type": { + "value": "PanelSpriteButton::ButtonFSM::SetPropertyMouseIsInside" + }, + "parameters": [ + "Object", + "Behavior", + "yes" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Comment", + "color": { + "b": 109, + "g": 230, + "r": 255, + "textB": 0, + "textG": 0, + "textR": 0 + }, + "comment": "Touches are always pressed, so ShouldCheckHovering doesn't matter." + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "PanelSpriteButton::ButtonFSM::SetPropertyTouchIsInside" + }, + "parameters": [ + "Object", + "Behavior", + "no" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "PanelSpriteButton::ButtonFSM::PropertyTouchId" + }, + "parameters": [ + "Object", + "Behavior", + "!=", + "0" + ] + }, + { + "type": { + "value": "CollisionPoint" + }, + "parameters": [ + "Object", + "TouchX(Object.Behavior::PropertyTouchId(), Object.Layer(), 0)", + "TouchY(Object.Behavior::PropertyTouchId(), Object.Layer(), 0)" + ] + } + ], + "actions": [ + { + "type": { + "value": "PanelSpriteButton::ButtonFSM::SetPropertyTouchIsInside" + }, + "parameters": [ + "Object", + "Behavior", + "yes" + ] + } + ] + } + ], + "parameters": [] + }, + { + "colorB": 228, + "colorG": 176, + "colorR": 74, + "creationTime": 0, + "name": "Handle touch start", + "source": "", + "type": "BuiltinCommonInstructions::Group", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "HasAnyTouchOrMouseStarted" + }, + "parameters": [ + "" + ] + } + ], + "actions": [ + { + "type": { + "value": "PanelSpriteButton::ButtonFSM::SetPropertyIndex" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "0" + ] + } + ], + "events": [ + { + "type": "BuiltinCommonInstructions::Repeat", + "repeatExpression": "StartedTouchOrMouseCount()", + "conditions": [], + "actions": [], + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "CollisionPoint" + }, + "parameters": [ + "Object", + "TouchX(StartedTouchOrMouseId(Object.Behavior::PropertyIndex()), Object.Layer(), 0)", + "TouchY(StartedTouchOrMouseId(Object.Behavior::PropertyIndex()), Object.Layer(), 0)" + ] + } + ], + "actions": [ + { + "type": { + "value": "PanelSpriteButton::ButtonFSM::SetPropertyTouchId" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "StartedTouchOrMouseId(Object.Behavior::PropertyIndex())" + ] + }, + { + "type": { + "value": "PanelSpriteButton::ButtonFSM::SetPropertyTouchIsInside" + }, + "parameters": [ + "Object", + "Behavior", + "yes" + ] + } + ], + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "BuiltinCommonInstructions::Or" + }, + "parameters": [], + "subInstructions": [ + { + "type": { + "value": "PanelSpriteButton::ButtonFSM::PropertyState" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "\"Hovered\"" + ] + }, + { + "type": { + "value": "PanelSpriteButton::ButtonFSM::PropertyState" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "\"Idle\"" + ] + } + ] + } + ], + "actions": [ + { + "type": { + "value": "PanelSpriteButton::ButtonFSM::SetPropertyState" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "\"PressedInside\"" + ] + } + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "PanelSpriteButton::ButtonFSM::SetPropertyIndex" + }, + "parameters": [ + "Object", + "Behavior", + "+", + "1" + ] + } + ] + } + ] + } + ] + } + ], + "parameters": [] + }, + { + "colorB": 228, + "colorG": 176, + "colorR": 74, + "creationTime": 0, + "name": "Apply position changes", + "source": "", + "type": "BuiltinCommonInstructions::Group", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "inverted": true, + "value": "PanelSpriteButton::ButtonFSM::PropertyMouseIsInside" + }, + "parameters": [ + "Object", + "Behavior" + ] + }, + { + "type": { + "value": "PanelSpriteButton::ButtonFSM::PropertyState" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "\"Hovered\"" + ] + } + ], + "actions": [ + { + "type": { + "value": "PanelSpriteButton::ButtonFSM::SetPropertyState" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "\"Idle\"" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "PanelSpriteButton::ButtonFSM::PropertyMouseIsInside" + }, + "parameters": [ + "Object", + "Behavior" + ] + }, + { + "type": { + "value": "PanelSpriteButton::ButtonFSM::PropertyState" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "\"Idle\"" + ] + } + ], + "actions": [ + { + "type": { + "value": "PanelSpriteButton::ButtonFSM::SetPropertyState" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "\"Hovered\"" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "inverted": true, + "value": "PanelSpriteButton::ButtonFSM::PropertyTouchIsInside" + }, + "parameters": [ + "Object", + "Behavior" + ] + }, + { + "type": { + "value": "PanelSpriteButton::ButtonFSM::PropertyState" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "\"PressedInside\"" + ] + } + ], + "actions": [ + { + "type": { + "value": "PanelSpriteButton::ButtonFSM::SetPropertyState" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "\"PressedOutside\"" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "PanelSpriteButton::ButtonFSM::PropertyTouchIsInside" + }, + "parameters": [ + "Object", + "Behavior" + ] + }, + { + "type": { + "value": "PanelSpriteButton::ButtonFSM::PropertyState" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "\"PressedOutside\"" + ] + } + ], + "actions": [ + { + "type": { + "value": "PanelSpriteButton::ButtonFSM::SetPropertyState" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "\"PressedInside\"" + ] + } + ] + } + ], + "parameters": [] + }, + { + "colorB": 228, + "colorG": 176, + "colorR": 74, + "creationTime": 0, + "name": "Handle touch end", + "source": "", + "type": "BuiltinCommonInstructions::Group", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "HasTouchEnded" + }, + "parameters": [ + "", + "Object.Behavior::PropertyTouchId()" + ] + } + ], + "actions": [ + { + "type": { + "value": "PanelSpriteButton::ButtonFSM::SetPropertyTouchId" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "0" + ] + } + ], + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "PanelSpriteButton::ButtonFSM::PropertyState" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "\"PressedInside\"" + ] + } + ], + "actions": [ + { + "type": { + "value": "PanelSpriteButton::ButtonFSM::SetPropertyState" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "\"Validated\"" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "inverted": true, + "value": "PanelSpriteButton::ButtonFSM::PropertyState" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "\"PressedInside\"" + ] + }, + { + "type": { + "inverted": true, + "value": "PanelSpriteButton::ButtonFSM::PropertyState" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "\"Validated\"" + ] + } + ], + "actions": [ + { + "type": { + "value": "PanelSpriteButton::ButtonFSM::SetPropertyState" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "\"Idle\"" + ] + } + ] + } + ] + } + ], + "parameters": [] + } + ], + "parameters": [] + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "PanelSpriteButton::ButtonFSM", + "type": "behavior" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onDeActivate", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "PanelSpriteButton::ButtonFSM::ResetState" + }, + "parameters": [ + "Object", + "Behavior", + "" + ] + } + ] + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "PanelSpriteButton::ButtonFSM", + "type": "behavior" + } + ], + "objectGroups": [] + }, + { + "description": "Reset the state of the button.", + "fullName": "Reset state", + "functionType": "Action", + "name": "ResetState", + "private": true, + "sentence": "Reset the button state of _PARAM0_", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "PanelSpriteButton::ButtonFSM::SetPropertyState" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "\"Idle\"" + ] + }, + { + "type": { + "value": "PanelSpriteButton::ButtonFSM::SetPropertyTouchId" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "0" + ] + } + ] + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "PanelSpriteButton::ButtonFSM", + "type": "behavior" + } + ], + "objectGroups": [] + }, + { + "description": "Check if the button is not used.", + "fullName": "Is idle", + "functionType": "Condition", + "name": "IsIdle", + "sentence": "_PARAM0_ is idle", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "PanelSpriteButton::ButtonFSM::PropertyState" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "\"Idle\"" + ] + } + ], + "actions": [ + { + "type": { + "value": "SetReturnBoolean" + }, + "parameters": [ + "True" + ] + } + ] + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "PanelSpriteButton::ButtonFSM", + "type": "behavior" + } + ], + "objectGroups": [] + }, + { + "description": "Check if the button was just clicked.", + "fullName": "Is clicked", + "functionType": "Condition", + "name": "IsClicked", + "sentence": "_PARAM0_ is clicked", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "PanelSpriteButton::ButtonFSM::PropertyState" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "\"Validated\"" + ] + } + ], + "actions": [ + { + "type": { + "value": "SetReturnBoolean" + }, + "parameters": [ + "True" + ] + } + ] + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "PanelSpriteButton::ButtonFSM", + "type": "behavior" + } + ], + "objectGroups": [] + }, + { + "description": "Check if the cursor is hovered over the button.", + "fullName": "Is hovered", + "functionType": "Condition", + "name": "IsHovered", + "sentence": "_PARAM0_ is hovered", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "PanelSpriteButton::ButtonFSM::PropertyState" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "\"Hovered\"" + ] + } + ], + "actions": [ + { + "type": { + "value": "SetReturnBoolean" + }, + "parameters": [ + "True" + ] + } + ] + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "PanelSpriteButton::ButtonFSM", + "type": "behavior" + } + ], + "objectGroups": [] + }, + { + "description": "Check if the button is either hovered or pressed but not hovered.", + "fullName": "Is focused", + "functionType": "Condition", + "name": "IsFocused", + "sentence": "_PARAM0_ is focused", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "PanelSpriteButton::ButtonFSM::PropertyState" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "\"Hovered\"" + ] + } + ], + "actions": [ + { + "type": { + "value": "SetReturnBoolean" + }, + "parameters": [ + "True" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "PanelSpriteButton::ButtonFSM::PropertyState" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "\"PressedOutside\"" + ] + } + ], + "actions": [ + { + "type": { + "value": "SetReturnBoolean" + }, + "parameters": [ + "True" + ] + } + ] + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "PanelSpriteButton::ButtonFSM", + "type": "behavior" + } + ], + "objectGroups": [] + }, + { + "description": "Check if the button is currently being pressed with mouse or touch.", + "fullName": "Is pressed", + "functionType": "Condition", + "name": "IsPressed", + "sentence": "_PARAM0_ is pressed", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "PanelSpriteButton::ButtonFSM::PropertyState" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "\"PressedInside\"" + ] + } + ], + "actions": [ + { + "type": { + "value": "SetReturnBoolean" + }, + "parameters": [ + "True" + ] + } + ] + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "PanelSpriteButton::ButtonFSM", + "type": "behavior" + } + ], + "objectGroups": [] + }, + { + "description": "Check if the button is currently being pressed outside with mouse or touch.", + "fullName": "Is held outside", + "functionType": "Condition", + "name": "IsPressedOutside", + "sentence": "_PARAM0_ is held outside", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "PanelSpriteButton::ButtonFSM::PropertyState" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "\"PressedOutside\"" + ] + } + ], + "actions": [ + { + "type": { + "value": "SetReturnBoolean" + }, + "parameters": [ + "True" + ] + } + ] + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "PanelSpriteButton::ButtonFSM", + "type": "behavior" + } + ], + "objectGroups": [] + }, + { + "description": "the touch id that is using the button or 0 if none.", + "fullName": "Touch id", + "functionType": "ExpressionAndCondition", + "name": "TouchId", + "sentence": "the touch id", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SetReturnNumber" + }, + "parameters": [ + "Object.Behavior::PropertyTouchId()" + ] + } + ] + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "PanelSpriteButton::ButtonFSM", + "type": "behavior" + } + ], + "objectGroups": [] + } + ], + "propertyDescriptors": [ + { + "value": "true", + "type": "Boolean", + "label": "", + "description": "Should check hovering", + "group": "", + "extraInformation": [], + "hidden": true, + "name": "ShouldCheckHovering" + }, + { + "value": "Idle", + "type": "Choice", + "label": "State", + "description": "", + "group": "", + "extraInformation": [ + "Idle", + "Hovered", + "PressedInside", + "PressedOutside", + "Validated" + ], + "hidden": true, + "name": "State" + }, + { + "value": "0", + "type": "Number", + "label": "Touch id", + "description": "", + "group": "", + "extraInformation": [], + "hidden": true, + "name": "TouchId" + }, + { + "value": "", + "type": "Boolean", + "label": "Touch is inside", + "description": "", + "group": "", + "extraInformation": [], + "hidden": true, + "name": "TouchIsInside" + }, + { + "value": "", + "type": "Boolean", + "label": "Mouse is inside", + "description": "", + "group": "", + "extraInformation": [], + "hidden": true, + "name": "MouseIsInside" + }, + { + "value": "", + "type": "Number", + "label": "", + "description": "", + "group": "", + "extraInformation": [], + "hidden": true, + "name": "Index" + } + ], + "sharedPropertyDescriptors": [] + } + ], + "eventsBasedObjects": [ + { + "defaultName": "Button", + "description": "A button that can be customized.", + "fullName": "Button (panel sprite)", + "name": "PanelSpriteButton", + "eventsFunctions": [ + { + "fullName": "", + "functionType": "Action", + "name": "onCreated", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Comment", + "color": { + "b": 109, + "g": 230, + "r": 255, + "textB": 0, + "textG": 0, + "textR": 0 + }, + "comment": "Create one background instance for of each state.\nOnly the instance for the current state is shown." + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Create" + }, + "parameters": [ + "", + "Idle", + "0", + "0", + "" + ] + }, + { + "type": { + "value": "Create" + }, + "parameters": [ + "", + "Hovered", + "0", + "0", + "" + ] + }, + { + "type": { + "value": "Create" + }, + "parameters": [ + "", + "Pressed", + "0", + "0", + "" + ] + }, + { + "type": { + "value": "Cache" + }, + "parameters": [ + "Hovered" + ] + }, + { + "type": { + "value": "Cache" + }, + "parameters": [ + "Pressed" + ] + }, + { + "type": { + "value": "ChangePlan" + }, + "parameters": [ + "Hovered", + "=", + "1" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Comment", + "color": { + "b": 109, + "g": 230, + "r": 255, + "textB": 0, + "textG": 0, + "textR": 0 + }, + "comment": "Place the label over the backgrounds." + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Create" + }, + "parameters": [ + "", + "Label", + "0", + "0", + "" + ] + }, + { + "type": { + "value": "ChangePlan" + }, + "parameters": [ + "Label", + "=", + "2" + ] + }, + { + "type": { + "value": "TextObject::SetWrapping" + }, + "parameters": [ + "Label", + "yes" + ] + }, + { + "type": { + "value": "PanelSpriteButton::PanelSpriteButton::CenterLabel" + }, + "parameters": [ + "Object", + "" + ] + } + ] + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "PanelSpriteButton::PanelSpriteButton", + "type": "object" + } + ], + "objectGroups": [ + { + "name": "Background", + "objects": [ + { + "name": "Idle" + }, + { + "name": "Hovered" + }, + { + "name": "Pressed" + } + ] + } + ] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onHotReloading", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "PanelSpriteButton::PanelSpriteButton::CenterLabel" + }, + "parameters": [ + "Object", + "" + ] + } + ] + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "PanelSpriteButton::PanelSpriteButton", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "doStepPostEvents", + "sentence": "", + "events": [ + { + "colorB": 228, + "colorG": 176, + "colorR": 74, + "creationTime": 0, + "name": "Apply states", + "source": "", + "type": "BuiltinCommonInstructions::Group", + "events": [ + { + "type": "BuiltinCommonInstructions::Comment", + "color": { + "b": 109, + "g": 230, + "r": 255, + "textB": 0, + "textG": 0, + "textR": 0 + }, + "comment": "Show the right background accordingly to the new state." + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "PanelSpriteButton::PanelSpriteButton::IsIdle" + }, + "parameters": [ + "Object", + "ButtonFSM" + ] + }, + { + "type": { + "value": "BuiltinCommonInstructions::Once" + }, + "parameters": [] + } + ], + "actions": [ + { + "type": { + "value": "SetCenterY" + }, + "parameters": [ + "Label", + "=", + "Object.CenterWithPaddingY()" + ] + }, + { + "type": { + "value": "Montre" + }, + "parameters": [ + "Idle", + "" + ] + }, + { + "type": { + "value": "Cache" + }, + "parameters": [ + "Pressed" + ] + } + ], + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "Visible" + }, + "parameters": [ + "Hovered" + ] + }, + { + "type": { + "value": "PanelSpriteButton::PanelSpriteButton::PropertyHoveredFadeOutDuration" + }, + "parameters": [ + "Object", + ">", + "0" + ] + } + ], + "actions": [ + { + "type": { + "value": "Tween::AddObjectOpacityTween" + }, + "parameters": [ + "Hovered", + "Tween", + "\"Fadeout\"", + "0", + "\"linear\"", + "Object.PropertyHoveredFadeOutDuration() * 1000", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "PanelSpriteButton::PanelSpriteButton::PropertyHoveredFadeOutDuration" + }, + "parameters": [ + "Object", + "=", + "0" + ] + } + ], + "actions": [ + { + "type": { + "value": "Cache" + }, + "parameters": [ + "Hovered" + ] + } + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "PanelSpriteObject::Opacity" + }, + "parameters": [ + "Hovered", + "=", + "0" + ] + } + ], + "actions": [ + { + "type": { + "value": "Cache" + }, + "parameters": [ + "Hovered" + ] + }, + { + "type": { + "value": "PanelSpriteObject::SetOpacity" + }, + "parameters": [ + "Hovered", + "=", + "255" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "PanelSpriteButton::PanelSpriteButton::IsHovered" + }, + "parameters": [ + "Object", + "ButtonFSM" + ] + }, + { + "type": { + "value": "BuiltinCommonInstructions::Once" + }, + "parameters": [] + } + ], + "actions": [ + { + "type": { + "value": "SetCenterY" + }, + "parameters": [ + "Label", + "=", + "Object.CenterWithPaddingY()" + ] + }, + { + "type": { + "value": "Cache" + }, + "parameters": [ + "Idle" + ] + }, + { + "type": { + "value": "Montre" + }, + "parameters": [ + "Hovered", + "" + ] + }, + { + "type": { + "value": "Cache" + }, + "parameters": [ + "Pressed" + ] + }, + { + "type": { + "value": "Tween::RemoveTween" + }, + "parameters": [ + "Hovered", + "Tween", + "\"Fadeout\"" + ] + }, + { + "type": { + "value": "PanelSpriteObject::SetOpacity" + }, + "parameters": [ + "Hovered", + "=", + "255" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "PanelSpriteButton::PanelSpriteButton::IsPressed" + }, + "parameters": [ + "Object", + "ButtonFSM" + ] + }, + { + "type": { + "value": "BuiltinCommonInstructions::Once" + }, + "parameters": [] + } + ], + "actions": [ + { + "type": { + "value": "SetCenterY" + }, + "parameters": [ + "Label", + "=", + "Object.CenterWithPaddingY() + Object.PropertyPressedLabelOffsetY()" + ] + }, + { + "type": { + "value": "Cache" + }, + "parameters": [ + "Idle" + ] + }, + { + "type": { + "value": "Cache" + }, + "parameters": [ + "Hovered" + ] + }, + { + "type": { + "value": "Montre" + }, + "parameters": [ + "Pressed", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "PanelSpriteButton::PanelSpriteButton::IsFocused" + }, + "parameters": [ + "Object", + "ButtonFSM" + ] + }, + { + "type": { + "value": "BuiltinCommonInstructions::Once" + }, + "parameters": [] + } + ], + "actions": [ + { + "type": { + "value": "SetCenterY" + }, + "parameters": [ + "Label", + "=", + "Object.CenterWithPaddingY()" + ] + }, + { + "type": { + "value": "Cache" + }, + "parameters": [ + "Idle" + ] + }, + { + "type": { + "value": "Montre" + }, + "parameters": [ + "Hovered", + "" + ] + }, + { + "type": { + "value": "Cache" + }, + "parameters": [ + "Pressed" + ] + } + ] + } + ], + "parameters": [] + }, + { + "colorB": 228, + "colorG": 176, + "colorR": 74, + "creationTime": 0, + "name": "Resize", + "source": "", + "type": "BuiltinCommonInstructions::Group", + "events": [ + { + "type": "BuiltinCommonInstructions::Comment", + "color": { + "b": 109, + "g": 230, + "r": 255, + "textB": 0, + "textG": 0, + "textR": 0 + }, + "comment": "Children instances must be resized when the button size change:\n- backgrounds for each state are resized to take the full dimensions of the button\n- the label is put back at the center of the button\n\nThe scale is set back to 1 because it means that the parent instance has the same dimensions as the union of its children instances." + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "BuiltinCommonInstructions::Or" + }, + "parameters": [], + "subInstructions": [ + { + "type": { + "value": "BuiltinCommonInstructions::CompareNumbers" + }, + "parameters": [ + "Object.Width()", + "!=", + "max(Idle.BoundingBoxRight(), Label.BoundingBoxRight()) - min(Idle.BoundingBoxLeft(), Label.BoundingBoxLeft())" + ] + }, + { + "type": { + "value": "BuiltinCommonInstructions::CompareNumbers" + }, + "parameters": [ + "Object.Height()", + "!=", + "max(Idle.BoundingBoxBottom(), Label.BoundingBoxBottom()) - min(Idle.BoundingBoxTop(), Label.BoundingBoxTop())" + ] + } + ] + } + ], + "actions": [ + { + "type": { + "value": "ModVarObjet" + }, + "parameters": [ + "Idle", + "Width", + "=", + "Object.Width()" + ] + }, + { + "type": { + "value": "ModVarObjet" + }, + "parameters": [ + "Idle", + "Height", + "=", + "Object.Height()" + ] + }, + { + "type": { + "value": "PanelSpriteButton::Scale" + }, + "parameters": [ + "Object", + "=", + "1" + ] + }, + { + "type": { + "value": "PanelSpriteObject::Width" + }, + "parameters": [ + "Background", + "=", + "Idle.Variable(Width)" + ] + }, + { + "type": { + "value": "PanelSpriteObject::Height" + }, + "parameters": [ + "Background", + "=", + "Idle.Variable(Height)" + ] + }, + { + "type": { + "value": "PanelSpriteButton::PanelSpriteButton::CenterLabel" + }, + "parameters": [ + "Object", + "" + ] + } + ] + } + ], + "parameters": [] + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "PanelSpriteButton::PanelSpriteButton", + "type": "object" + } + ], + "objectGroups": [ + { + "name": "Background", + "objects": [ + { + "name": "Idle" + }, + { + "name": "Hovered" + }, + { + "name": "Pressed" + } + ] + } + ] + }, + { + "description": "Check if the button is not used.", + "fullName": "Is idle", + "functionType": "Condition", + "name": "IsIdle", + "sentence": "_PARAM0_ is idle", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "PanelSpriteButton::ButtonFSM::IsIdle" + }, + "parameters": [ + "Idle", + "ButtonFSM", + "" + ] + } + ], + "actions": [ + { + "type": { + "value": "SetReturnBoolean" + }, + "parameters": [ + "True" + ] + } + ] + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "PanelSpriteButton::PanelSpriteButton", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "description": "Check if the button was just clicked.", + "fullName": "Is clicked", + "functionType": "Condition", + "name": "IsClicked", + "sentence": "_PARAM0_ is clicked", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "PanelSpriteButton::ButtonFSM::IsClicked" + }, + "parameters": [ + "Idle", + "ButtonFSM", + "" + ] + } + ], + "actions": [ + { + "type": { + "value": "SetReturnBoolean" + }, + "parameters": [ + "True" + ] + } + ] + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "PanelSpriteButton::PanelSpriteButton", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "description": "Check if the cursor is hovered over the button.", + "fullName": "Is hovered", + "functionType": "Condition", + "name": "IsHovered", + "sentence": "_PARAM0_ is hovered", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "PanelSpriteButton::ButtonFSM::IsHovered" + }, + "parameters": [ + "Idle", + "ButtonFSM", + "" + ] + } + ], + "actions": [ + { + "type": { + "value": "SetReturnBoolean" + }, + "parameters": [ + "True" + ] + } + ] + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "PanelSpriteButton::PanelSpriteButton", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "description": "Check if the button is either hovered or pressed but not hovered.", + "fullName": "Is focused", + "functionType": "Condition", + "name": "IsFocused", + "sentence": "_PARAM0_ is focused", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "PanelSpriteButton::ButtonFSM::IsFocused" + }, + "parameters": [ + "Idle", + "ButtonFSM", + "" + ] + } + ], + "actions": [ + { + "type": { + "value": "SetReturnBoolean" + }, + "parameters": [ + "True" + ] + } + ] + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "PanelSpriteButton::PanelSpriteButton", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "description": "Check if the button is currently being pressed with mouse or touch.", + "fullName": "Is pressed", + "functionType": "Condition", + "name": "IsPressed", + "sentence": "_PARAM0_ is pressed", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "PanelSpriteButton::ButtonFSM::IsPressed" + }, + "parameters": [ + "Idle", + "ButtonFSM", + "" + ] + } + ], + "actions": [ + { + "type": { + "value": "SetReturnBoolean" + }, + "parameters": [ + "True" + ] + } + ] + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "PanelSpriteButton::PanelSpriteButton", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "description": "Change the text of the button label.", + "fullName": "Label text", + "functionType": "Action", + "name": "SetLabelText", + "sentence": "Change the text of _PARAM0_ to _PARAM1_", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "TextObject::String" + }, + "parameters": [ + "Label", + "=", + "GetArgumentAsString(\"LabelText\")" + ] + }, + { + "type": { + "value": "PanelSpriteButton::PanelSpriteButton::CenterLabel" + }, + "parameters": [ + "Object", + "" + ] + } + ] + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "PanelSpriteButton::PanelSpriteButton", + "type": "object" + }, + { + "description": "Text", + "name": "LabelText", + "type": "string" + } + ], + "objectGroups": [] + }, + { + "description": "Return the label text.", + "fullName": "Label text", + "functionType": "StringExpression", + "name": "LabelText", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SetReturnString" + }, + "parameters": [ + "Label.String()" + ] + } + ] + } + ], + "expressionType": { + "type": "string" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "PanelSpriteButton::PanelSpriteButton", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "description": "Return the label center Y according to the button configuration. This expression is used in doStepPostEvents when the button is pressed or released.", + "fullName": "", + "functionType": "Expression", + "name": "CenterWithPaddingY", + "private": true, + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SetReturnNumber" + }, + "parameters": [ + "Idle.CenterY() + (Object.PropertyTopPadding() - Object.PropertyBottomPadding()) / 2" + ] + } + ] + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "PanelSpriteButton::PanelSpriteButton", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "description": "Center the label according to the button configuration. This is used in doStepPostEvents when the button is resized.", + "fullName": "", + "functionType": "Action", + "name": "CenterLabel", + "private": true, + "sentence": "Center the label of _PARAM0_", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "MettreXY" + }, + "parameters": [ + "Label", + "=", + "Object.PropertyLeftPadding()", + "=", + "Object.PropertyTopPadding()" + ] + }, + { + "type": { + "value": "TextObject::WrappingWidth" + }, + "parameters": [ + "Label", + "=", + "Idle.Width() - Object.PropertyLeftPadding() - Object.PropertyRightPadding()" + ] + }, + { + "type": { + "value": "SetCenterY" + }, + "parameters": [ + "Label", + "=", + "Object.CenterWithPaddingY()" + ] + } + ], + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SetCenterX" + }, + "parameters": [ + "Label", + "=", + "Background.CenterX() + (Object.PropertyLeftPadding() - Object.PropertyRightPadding()) / 2" + ] + } + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "PanelSpriteButton::PanelSpriteButton::IsPressed" + }, + "parameters": [ + "Object", + "" + ] + } + ], + "actions": [ + { + "type": { + "value": "MettreY" + }, + "parameters": [ + "Label", + "+", + "Object.PropertyPressedLabelOffsetY()" + ] + } + ] + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "PanelSpriteButton::PanelSpriteButton", + "type": "object" + } + ], + "objectGroups": [ + { + "name": "Background", + "objects": [ + { + "name": "Idle" + }, + { + "name": "Hovered" + }, + { + "name": "Pressed" + } + ] + } + ] + }, + { + "description": "De/activate interactions with the button.", + "fullName": "De/activate interactions", + "functionType": "Action", + "name": "Activate", + "sentence": "Activate interactions with _PARAM0_: _PARAM1_", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "GetArgumentAsBoolean" + }, + "parameters": [ + "\"ShouldActivate\"" + ] + } + ], + "actions": [ + { + "type": { + "value": "ActivateBehavior" + }, + "parameters": [ + "Idle", + "ButtonFSM", + "yes" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "inverted": true, + "value": "GetArgumentAsBoolean" + }, + "parameters": [ + "\"ShouldActivate\"" + ] + } + ], + "actions": [ + { + "type": { + "value": "ActivateBehavior" + }, + "parameters": [ + "Idle", + "ButtonFSM", + "no" + ] + } + ] + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "PanelSpriteButton::PanelSpriteButton", + "type": "object" + }, + { + "description": "Activate", + "name": "ShouldActivate", + "type": "yesorno" + } + ], + "objectGroups": [] + }, + { + "description": "Check if interactions are activated on the button.", + "fullName": "Interactions activated", + "functionType": "Condition", + "name": "IsActivated", + "sentence": "Interactions on _PARAM0_ are activated", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "BehaviorActivated" + }, + "parameters": [ + "Idle", + "ButtonFSM" + ] + } + ], + "actions": [ + { + "type": { + "value": "SetReturnBoolean" + }, + "parameters": [ + "True" + ] + } + ] + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "PanelSpriteButton::PanelSpriteButton", + "type": "object" + } + ], + "objectGroups": [] + } + ], + "propertyDescriptors": [ + { + "value": "0", + "type": "Number", + "label": "Label offset on Y axis when pressed", + "description": "", + "group": "", + "extraInformation": [], + "hidden": false, + "name": "PressedLabelOffsetY" + }, + { + "value": "0", + "type": "Number", + "label": "Left padding", + "description": "", + "group": "Padding", + "extraInformation": [ + "Label" + ], + "hidden": false, + "name": "LeftPadding" + }, + { + "value": "0", + "type": "Number", + "label": "Right padding", + "description": "", + "group": "Padding", + "extraInformation": [ + "Label" + ], + "hidden": false, + "name": "RightPadding" + }, + { + "value": "0", + "type": "Number", + "label": "Top padding", + "description": "", + "group": "Padding", + "extraInformation": [ + "Label" + ], + "hidden": false, + "name": "TopPadding" + }, + { + "value": "0", + "type": "Number", + "label": "Bottom padding", + "description": "", + "group": "Padding", + "extraInformation": [ + "Label" + ], + "hidden": false, + "name": "BottomPadding" + }, + { + "value": "0.25", + "type": "Number", + "label": "Hovered fade out duration (in seconds)", + "description": "", + "group": "", + "extraInformation": [], + "hidden": false, + "name": "HoveredFadeOutDuration" + } + ], + "objects": [ + { + "assetStoreId": "", + "bold": false, + "italic": false, + "name": "Label", + "smoothed": true, + "tags": "", + "type": "TextObject::Text", + "underlined": false, + "variables": [], + "effects": [], + "behaviors": [], + "string": "Text", + "font": "", + "textAlignment": "", + "characterSize": 20, + "color": { + "b": 0, + "g": 0, + "r": 0 + } + }, + { + "assetStoreId": "", + "bottomMargin": 0, + "height": 32, + "leftMargin": 0, + "name": "Idle", + "rightMargin": 0, + "tags": "", + "texture": "", + "tiled": false, + "topMargin": 0, + "type": "PanelSpriteObject::PanelSprite", + "width": 32, + "variables": [ + { + "folded": true, + "name": "State", + "type": "string", + "value": "Idle" + } + ], + "effects": [], + "behaviors": [ + { + "name": "ButtonFSM", + "type": "PanelSpriteButton::ButtonFSM", + "ShouldCheckHovering": true + } + ] + }, + { + "assetStoreId": "", + "bottomMargin": 0, + "height": 32, + "leftMargin": 0, + "name": "Hovered", + "rightMargin": 0, + "tags": "", + "texture": "", + "tiled": false, + "topMargin": 0, + "type": "PanelSpriteObject::PanelSprite", + "width": 32, + "variables": [], + "effects": [], + "behaviors": [ + { + "name": "Tween", + "type": "Tween::TweenBehavior" + } + ] + }, + { + "assetStoreId": "", + "bottomMargin": 0, + "height": 32, + "leftMargin": 0, + "name": "Pressed", + "rightMargin": 0, + "tags": "", + "texture": "", + "tiled": false, + "topMargin": 0, + "type": "PanelSpriteObject::PanelSprite", + "width": 32, + "variables": [], + "effects": [], + "behaviors": [] + } + ] + } + ] + } + ], + "externalLayouts": [], + "externalSourceFiles": [] +} \ No newline at end of file diff --git a/GDJS/tests/tests/variable.js b/GDJS/tests/tests/variable.js index d017c838c5..4d99b1ac7f 100644 --- a/GDJS/tests/tests/variable.js +++ b/GDJS/tests/tests/variable.js @@ -17,6 +17,7 @@ describe('gdjs.Variable', function () { expect(intVar.getAsNumber()).to.be(526); expect(intVar.getAsString()).to.be('526'); + expect(intVar.getAsNumberOrString()).to.be(526); expect(intVar.getAsBoolean()).to.be(true); expect(intVar.getType()).to.be('number'); @@ -25,11 +26,13 @@ describe('gdjs.Variable', function () { expect(floatVar.getAsNumber()).to.be(10.568); expect(floatVar.getAsString()).to.be('10.568'); + expect(floatVar.getAsNumberOrString()).to.be(10.568); expect(floatVar.getAsBoolean()).to.be(true); expect(floatVar.getType()).to.be('number'); expect(strVar.getAsNumber()).to.be(0); expect(strVar.getAsString()).to.be('test variable'); + expect(strVar.getAsNumberOrString()).to.be('test variable'); expect(strVar.getAsBoolean()).to.be(true); expect(strVar.getType()).to.be('string'); @@ -45,12 +48,14 @@ describe('gdjs.Variable', function () { expect(numStrVar.getAsNumber()).to.be(5); expect(numStrVar.getAsString()).to.be('5Apples'); expect(numStrVar.getAsBoolean()).to.be(true); + expect(numStrVar.getAsNumberOrString()).to.be('5Apples'); expect(numStrVar.getType()).to.be('string'); expect(boolVar.getType()).to.be('boolean'); expect(boolVar.getAsString()).to.be('true'); expect(boolVar.getAsNumber()).to.be(1); expect(boolVar.getAsBoolean()).to.be(true); + expect(boolVar.getAsNumberOrString()).to.be('true'); }); it('should do some variable arithmetics', function () { @@ -66,6 +71,7 @@ describe('gdjs.Variable', function () { expect(a.getAsNumber()).to.be(3); a.concatenateString('Apples'); expect(a.getAsString()).to.be('3Apples'); + expect(a.getAsNumberOrString()).to.be('3Apples'); }); it('should clear a collection', function () { diff --git a/GDevelop.js/Bindings/Bindings.idl b/GDevelop.js/Bindings/Bindings.idl index d0069b7860..b81aa50303 100644 --- a/GDevelop.js/Bindings/Bindings.idl +++ b/GDevelop.js/Bindings/Bindings.idl @@ -2600,7 +2600,7 @@ interface ExpressionNodeLocationFinder { }; interface ExpressionTypeFinder { - [Const, Ref] DOMString STATIC_GetType([Const, Ref] Platform platform, [Const, Ref] ObjectsContainersList objectsContainersList, [Const] DOMString rootType, [Ref] ExpressionNode node); + [Const, Ref] DOMString STATIC_GetType([Const, Ref] Platform platform, [Const, Ref] ProjectScopedContainers projectScopedContainers, [Const] DOMString rootType, [Ref] ExpressionNode node); }; interface ExpressionNode { diff --git a/GDevelop.js/TestUtils/GDJSMocks.js b/GDevelop.js/TestUtils/GDJSMocks.js index 24a067dc2c..ba4325faa5 100644 --- a/GDevelop.js/TestUtils/GDJSMocks.js +++ b/GDevelop.js/TestUtils/GDJSMocks.js @@ -191,6 +191,10 @@ class Variable { this.setString(this.getAsString() + str); } + getAsNumberOrString() { + return this._value; + } + addChild(childName, childVariable) { // Make sure this is a structure this.castTo('structure'); diff --git a/GDevelop.js/__tests__/GDJSLayoutCodeGenerationIntegrationTests.js b/GDevelop.js/__tests__/GDJSLayoutCodeGenerationIntegrationTests.js index 1df2fb1235..274c50ed61 100644 --- a/GDevelop.js/__tests__/GDJSLayoutCodeGenerationIntegrationTests.js +++ b/GDevelop.js/__tests__/GDJSLayoutCodeGenerationIntegrationTests.js @@ -171,109 +171,198 @@ describe('libGD.js - GDJS Scene Code Generation integration tests', function () project.delete(); }); - // TODO: this does not pass because of string variables not understood as strings inside brackets. - // it('generates code for scene variables, including structures being accessed by other variables', function () { - // const project = new gd.ProjectHelper.createNewGDJSProject(); - // const layout = project.insertNewLayout('Scene', 0); - // layout.insertNewObject(project, '', 'MyObject', 0); - // layout.getVariables().insertNew('MyNumberVariable', 0).setValue(123); - // layout.getVariables().insertNew('MyStringVariable', 1).setString('Test'); - // layout - // .getVariables() - // .insertNew('MyOtherStringVariable', 1) - // .setString('SomeChild'); - // layout - // .getVariables() - // .insertNew('MyOtherStringVariable2', 1) - // .setString('SomeOtherChild'); - // const structureVariable = layout - // .getVariables() - // .insertNew('MyStructureVariable', 2); - // structureVariable.getChild('Test').setValue(1); - // structureVariable.getChild('123').setValue(2); - // structureVariable.getChild('42').setValue(4); - // structureVariable.getChild('MyObject').setValue(8); - // structureVariable.getChild('SomeChild').getChild('Test').setValue(16); - // structureVariable.getChild('SomeChild').getChild('123').setValue(32); - // structureVariable.getChild('SomeChild').getChild('42').setValue(64); - // structureVariable.getChild('SomeChild').getChild('MyObject').setValue(128); - // structureVariable - // .getChild('SomeChild') - // .getChild('SomeOtherChild') - // .getChild('Test') - // .setValue(256); - // structureVariable - // .getChild('SomeChild') - // .getChild('SomeOtherChild') - // .getChild('123') - // .setValue(512); - // structureVariable - // .getChild('SomeChild') - // .getChild('SomeOtherChild') - // .getChild('42') - // .setValue(1024); - // structureVariable - // .getChild('SomeChild') - // .getChild('SomeOtherChild') - // .getChild('MyObject') - // .setValue(2048); - // const serializedLayoutEvents = gd.Serializer.fromJSObject([ - // { - // type: 'BuiltinCommonInstructions::Standard', - // conditions: [], - // actions: [ - // { - // type: { value: 'ModVarScene' }, - // parameters: [ - // 'Counter', - // '+', - // 'MyStructureVariable[MyStringVariable] + MyStructureVariable[MyNumberVariable] + MyStructureVariable[MyObject.X()] + MyStructureVariable[MyObject.ObjectName()]', - // ], - // }, - // { - // type: { value: 'ModVarScene' }, - // parameters: [ - // 'Counter', - // '+', - // 'MyStructureVariable.SomeChild[MyStringVariable] + MyStructureVariable.SomeChild[MyNumberVariable] + MyStructureVariable.SomeChild[MyObject.X()] + MyStructureVariable.SomeChild[MyObject.ObjectName()]', - // ], - // }, - // { - // type: { value: 'ModVarScene' }, - // parameters: [ - // 'Counter', - // '+', - // 'MyStructureVariable[MyOtherStringVariable][MyOtherStringVariable2][MyStringVariable] +MyStructureVariable[MyOtherStringVariable][MyOtherStringVariable2][MyNumberVariable] + MyStructureVariable[MyOtherStringVariable][MyOtherStringVariable2][MyObject.X()] + MyStructureVariable[MyOtherStringVariable][MyOtherStringVariable2][MyObject.ObjectName()]', - // ], - // }, - // ], - // events: [], - // }, - // ]); - // layout.getEvents().unserializeFrom(project, serializedLayoutEvents); + describe('(Scene) variable code generation', () => { + let project = null; + let layout = null; + beforeEach(() => { + project = new gd.ProjectHelper.createNewGDJSProject(); + layout = project.insertNewLayout('Scene', 0); + layout.insertNewObject(project, '', 'MyObject', 0); - // const runCompiledEvents = generateCompiledEventsForLayout( - // gd, - // project, - // layout - // ); + // These variables are "simple" and their type will be known at code generation. + layout.getVariables().insertNew('MyNumberVariable', 0).setValue(123); + layout.getVariables().insertNew('MyStringVariable', 1).setString('Test'); + layout + .getVariables() + .insertNew('MyOtherStringVariable', 1) + .setString('SomeChild'); - // const serializedSceneElement = new gd.SerializerElement(); - // layout.serializeTo(serializedSceneElement); + // Use a variable that has a value deep inside a structure - so code generation + // does not know its type and will issue a `getAsNumberOrString`. + layout + .getVariables() + .insertNew('MyOtherStructureVariable', 1) + .getChild('Child') + .getChild('SubChild') + .setString('SomeOtherChild'); + const structureVariable = layout + .getVariables() + .insertNew('MyStructureVariable', 2); - // const { gdjs, runtimeScene } = makeMinimalGDJSMock({ - // sceneData: JSON.parse(gd.Serializer.toJSON(serializedSceneElement)), - // }); - // serializedSceneElement.delete(); - // const myObjectInstance = runtimeScene.createObject('MyObject'); - // myObjectInstance.setX(42); - // runCompiledEvents(gdjs, runtimeScene); + // Make a structure that we will address using other variables. + structureVariable.getChild('Test').setValue(1); + structureVariable.getChild('123').setValue(2); + structureVariable.getChild('42').setValue(4); + structureVariable.getChild('MyObject').setValue(8); + structureVariable.getChild('SomeChild').getChild('Test').setValue(16); + structureVariable.getChild('SomeChild').getChild('123').setValue(32); + structureVariable.getChild('SomeChild').getChild('42').setValue(64); + structureVariable + .getChild('SomeChild') + .getChild('MyObject') + .setValue(128); + structureVariable + .getChild('SomeChild') + .getChild('SomeOtherChild') + .getChild('Test') + .setValue(256); + structureVariable + .getChild('SomeChild') + .getChild('SomeOtherChild') + .getChild('123') + .setValue(512); + structureVariable + .getChild('SomeChild') + .getChild('SomeOtherChild') + .getChild('42') + .setValue(1024); + structureVariable + .getChild('SomeChild') + .getChild('SomeOtherChild') + .getChild('MyObject') + .setValue(2048); + }); + afterEach(() => { + project.delete(); + }); - // expect(runtimeScene.getVariables().has('Counter')).toBe(true); - // expect(runtimeScene.getVariables().get('Counter').getAsNumber()).toBe( - // 1 + 2 + 4 + 8 + 16 + 32 + 64 + 128 + 256 + 512 + 1024 + 2048 - // ); + it('generates code for structure variables accessed by another variable (1 level)', function () { + const serializedLayoutEvents = gd.Serializer.fromJSObject([ + { + type: 'BuiltinCommonInstructions::Standard', + conditions: [], + actions: [ + { + type: { value: 'ModVarScene' }, + parameters: [ + 'Counter', + '+', + 'MyStructureVariable[MyStringVariable] + MyStructureVariable[MyNumberVariable] + MyStructureVariable[MyObject.X()] + MyStructureVariable[MyObject.ObjectName()]', + ], + }, + ], + events: [], + }, + ]); + layout.getEvents().unserializeFrom(project, serializedLayoutEvents); - // project.delete(); - // }); + const runCompiledEvents = generateCompiledEventsForLayout( + gd, + project, + layout + ); + + const serializedSceneElement = new gd.SerializerElement(); + layout.serializeTo(serializedSceneElement); + + const { gdjs, runtimeScene } = makeMinimalGDJSMock({ + sceneData: JSON.parse(gd.Serializer.toJSON(serializedSceneElement)), + }); + serializedSceneElement.delete(); + const myObjectInstance = runtimeScene.createObject('MyObject'); + myObjectInstance.setX(42); + runCompiledEvents(gdjs, runtimeScene); + + expect(runtimeScene.getVariables().has('Counter')).toBe(true); + expect(runtimeScene.getVariables().get('Counter').getAsNumber()).toBe( + 1 + 2 + 4 + 8 + ); + }); + + it('generates code for structure variables accessed by another variable (2 levels)', function () { + const serializedLayoutEvents = gd.Serializer.fromJSObject([ + { + type: 'BuiltinCommonInstructions::Standard', + conditions: [], + actions: [ + { + type: { value: 'ModVarScene' }, + parameters: [ + 'Counter', + '+', + 'MyStructureVariable.SomeChild[MyStringVariable] + MyStructureVariable.SomeChild[MyNumberVariable] + MyStructureVariable.SomeChild[MyObject.X()] + MyStructureVariable.SomeChild[MyObject.ObjectName()]', + ], + }, + ], + events: [], + }, + ]); + layout.getEvents().unserializeFrom(project, serializedLayoutEvents); + + const runCompiledEvents = generateCompiledEventsForLayout( + gd, + project, + layout + ); + + const serializedSceneElement = new gd.SerializerElement(); + layout.serializeTo(serializedSceneElement); + + const { gdjs, runtimeScene } = makeMinimalGDJSMock({ + sceneData: JSON.parse(gd.Serializer.toJSON(serializedSceneElement)), + }); + serializedSceneElement.delete(); + const myObjectInstance = runtimeScene.createObject('MyObject'); + myObjectInstance.setX(42); + runCompiledEvents(gdjs, runtimeScene); + + expect(runtimeScene.getVariables().has('Counter')).toBe(true); + expect(runtimeScene.getVariables().get('Counter').getAsNumber()).toBe( + 16 + 32 + 64 + 128 + ); + }); + + it('generates code for structure variables accessed by another variable (3 levels)', function () { + const serializedLayoutEvents = gd.Serializer.fromJSObject([ + { + type: 'BuiltinCommonInstructions::Standard', + conditions: [], + actions: [ + { + type: { value: 'ModVarScene' }, + parameters: [ + 'Counter', + '+', + 'MyStructureVariable[MyOtherStringVariable][MyOtherStructureVariable.Child.SubChild][MyStringVariable] +MyStructureVariable[MyOtherStringVariable][MyOtherStructureVariable.Child.SubChild][MyNumberVariable] + MyStructureVariable[MyOtherStringVariable][MyOtherStructureVariable.Child.SubChild][MyObject.X()] + MyStructureVariable[MyOtherStringVariable][MyOtherStructureVariable.Child.SubChild][MyObject.ObjectName()]', + ], + }, + ], + events: [], + }, + ]); + layout.getEvents().unserializeFrom(project, serializedLayoutEvents); + + const runCompiledEvents = generateCompiledEventsForLayout( + gd, + project, + layout + ); + + const serializedSceneElement = new gd.SerializerElement(); + layout.serializeTo(serializedSceneElement); + + const { gdjs, runtimeScene } = makeMinimalGDJSMock({ + sceneData: JSON.parse(gd.Serializer.toJSON(serializedSceneElement)), + }); + serializedSceneElement.delete(); + const myObjectInstance = runtimeScene.createObject('MyObject'); + myObjectInstance.setX(42); + runCompiledEvents(gdjs, runtimeScene); + + expect(runtimeScene.getVariables().has('Counter')).toBe(true); + expect(runtimeScene.getVariables().get('Counter').getAsNumber()).toBe( + 256 + 512 + 1024 + 2048 + ); + }); + }); }); diff --git a/GDevelop.js/types/gdexpressiontypefinder.js b/GDevelop.js/types/gdexpressiontypefinder.js index 7e88fc5900..4d27a00495 100644 --- a/GDevelop.js/types/gdexpressiontypefinder.js +++ b/GDevelop.js/types/gdexpressiontypefinder.js @@ -1,6 +1,6 @@ // Automatically generated by GDevelop.js/scripts/generate-types.js declare class gdExpressionTypeFinder { - static getType(platform: gdPlatform, objectsContainersList: gdObjectsContainersList, rootType: string, node: gdExpressionNode): string; + static getType(platform: gdPlatform, projectScopedContainers: gdProjectScopedContainers, rootType: string, node: gdExpressionNode): string; delete(): void; ptr: number; }; \ No newline at end of file diff --git a/newIDE/app/src/EventsSheet/ParameterFields/GenericExpressionField/index.js b/newIDE/app/src/EventsSheet/ParameterFields/GenericExpressionField/index.js index 76aa1a28ed..7487019fb4 100644 --- a/newIDE/app/src/EventsSheet/ParameterFields/GenericExpressionField/index.js +++ b/newIDE/app/src/EventsSheet/ParameterFields/GenericExpressionField/index.js @@ -273,6 +273,7 @@ export default class ExpressionField extends React.Component { const { globalObjectsContainer, objectsContainer, + scope, expressionType, value, } = this.props; @@ -296,12 +297,14 @@ export default class ExpressionField extends React.Component { expressionNode, cursorPosition + 'fakeIdentifier'.length - 1 ); + const projectScopedContainers = getProjectScopedContainersFromScope( + scope, + globalObjectsContainer, + objectsContainer + ); const type = gd.ExpressionTypeFinder.getType( gd.JsPlatform.get(), - gd.ObjectsContainersList.makeNewObjectsContainersListForContainers( - globalObjectsContainer, - objectsContainer - ), + projectScopedContainers, expressionType, currentNode );