From 913e367f6f333f790f536fb0f1cf430ea397d479 Mon Sep 17 00:00:00 2001 From: Florian Rival Date: Mon, 9 Oct 2023 18:21:49 +0200 Subject: [PATCH] Allow to properly use variables/properties/parameters in brackets to access a structure variable (#5738) * This means expressions like `MyStructure[SomeIndexVariable]` will work properly for a structure if `SomeIndexVariable` is a string variable. Gdevelop now properly uses the type of the variable/property/parameter (to avoid considering what's inside the brackets as a number if it's a string). Remember to declare your variables in the variables editor so that you can use them in expressions. --- .../CodeGeneration/EventsCodeGenerator.cpp | 4 +- .../CodeGeneration/EventsCodeGenerator.h | 30 +- .../ExpressionCodeGenerator.cpp | 12 +- .../IDE/Events/EventsContextAnalyzer.cpp | 4 +- Core/GDCore/IDE/Events/EventsRefactorer.cpp | 10 +- .../IDE/Events/ExpressionCompletionFinder.h | 24 +- .../IDE/Events/ExpressionLeftSideTypeFinder.h | 118 +- Core/GDCore/IDE/Events/ExpressionTypeFinder.h | 25 +- .../GDCore/IDE/Events/ExpressionValidator.cpp | 37 +- Core/GDCore/IDE/Events/ExpressionValidator.h | 26 +- .../IDE/Events/UsedExtensionsFinder.cpp | 4 +- Core/GDCore/Project/ObjectsContainersList.cpp | 49 + Core/GDCore/Project/ObjectsContainersList.h | 7 + Core/tests/ExpressionCodeGenerator.cpp | 340 +- Core/tests/ExpressionParser2.cpp | 596 ++- .../CodeGeneration/EventsCodeGenerator.cpp | 22 +- .../CodeGeneration/EventsCodeGenerator.h | 6 + GDJS/Runtime/variable.ts | 69 +- GDJS/Runtime/variablescontainer.ts | 4 + .../assets/ArchitectsDaughter.ttf | Bin 0 -> 48640 bytes .../assets/Black Decorated Button_Hovered.png | Bin 0 -> 897 bytes .../assets/Black Decorated Button_Idle.png | Bin 0 -> 877 bytes .../assets/Black Decorated Button_Pressed.png | Bin 0 -> 878 bytes .../structure-variables-foreach.json | 3884 +++++++++++++++++ GDJS/tests/tests/variable.js | 6 + GDevelop.js/Bindings/Bindings.idl | 2 +- GDevelop.js/TestUtils/GDJSMocks.js | 4 + ...DJSLayoutCodeGenerationIntegrationTests.js | 289 +- GDevelop.js/types/gdexpressiontypefinder.js | 2 +- .../GenericExpressionField/index.js | 11 +- 30 files changed, 5328 insertions(+), 257 deletions(-) create mode 100644 GDJS/tests/games/structure-variables-foreach/assets/ArchitectsDaughter.ttf create mode 100644 GDJS/tests/games/structure-variables-foreach/assets/Black Decorated Button_Hovered.png create mode 100644 GDJS/tests/games/structure-variables-foreach/assets/Black Decorated Button_Idle.png create mode 100644 GDJS/tests/games/structure-variables-foreach/assets/Black Decorated Button_Pressed.png create mode 100644 GDJS/tests/games/structure-variables-foreach/structure-variables-foreach.json 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 0000000000000000000000000000000000000000..f4469b701358ce1f144ea7084db91bdb10bc0eb7 GIT binary patch literal 48640 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..c07f8106fe42af0eb5d861c2df4c435c2da06d35 GIT binary patch literal 897 zcmeAS@N?(olHy`uVBq!ia0vp^2Y}dtgAGXf&}T^g#gvld?7^XCmT>ygrh~!UQOmoMM6Y15+k+v{@K*8T8a z_t~|(zPft$A@^mvdU~f0A9faLnrta)`~CFCkA_G4&%FNryQrW*p!V+y$2+l>!Rvv?X6M14ELXWuDp5kCTD-tlgG#BTzJA8 z=iYeYieQ2fL%o%i)g-T_N)7sa{QQT_&TjeTC(7V^;nuBL4Er*+Ms3=#!Qkun%&)fU znlC;ur8ur)xO3+Y#{=Qm-CoJh7&1P-mSOndxiZA7G}%w4VfSw9$kuuPDu996D{ciX<-`sdx?za>t+yrahVk>G<@ue3hSXUccYzmlGw-nrgirs3}0 z*vhJ^NvBVHyD?X|X9r~mmv>)@^38f?dEno_eKGOz+z*8JzRu1)r*&1OqP8}cVg3@$ zx}(twDNeU~rGr@)1g+$FF!}T6V$Wo|ldiiS&d9R~TYdG(zkhZ%Om-J%7cIUxVej6( z9|U9XN4(0L_+Kc0@~Ucv&Hwe+{K%etrMH)tQAb0pyU<>a@r&W|01X2!h07&-7euW+ z*1hW^#{%^)?U7=F4d0!(?dPBWANEH}j*+1|f*l-KnGABQ8(u~-fAD3Ptydzn=ZV$* z*wdT|?$#Vl{KfAcFaI5ML73^rg=v9j`_&HWu_Q{xeBYdXo+;(>;%xPGxmjmlcg6=V XVx9IU+|3A>V;DSL{an^LB{Ts5P-}$k literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..7bca4b2e668475e4b74718558ead451d29f063e6 GIT binary patch literal 877 zcmeAS@N?(olHy`uVBq!ia0vp^2Y}dtgAGXfuA5M zvo{?}XGKKbjJPS&Yo;dtm)U1acyf~R+1P{f47{QzD@9A(UnFWS+}$>7_Vyht`a4e> zoIF~3|NCT{G6j}d-=ovYJk=(NUw&C~Sa#ozTh*Eh`McK1Ez)-~m! z0t)}z)_sc=Xv<_sD%a>ap1gDa{^@o5^(UWnX|U0pe!8ftN^AD)*}qC&WSJM8{eP2p zU)JaO^XFgwaboM%t)`VVH9pP-dzCg3$rI-mbqW$?xbpH~kaD9qHm`nr^H)1xO(R92?0^=O!&&-8Ek;>F6Gf+7rmT^T+p zXfW*JW7w3En7Dh&QsxC`|9|X?2>_fOK_`kTky}jPCJ3&F8gTY6c!JwDnfC&@B zW$JSFFfG{qcB8J)J?^*HkIE|jKl)qz!KY7`emggAU!?wO^JZh9dvYhQI`y9+K1++?mgYP=Gwn`z}&&$>FVdQ&MBb@0Q`M{ AuK)l5 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..b22d072e4a954c18e85c9e1f55c18a962fcf04dd GIT binary patch literal 878 zcmeAS@N?(olHy`uVBq!ia0vp^2Y}dtgAGXf0`Ik`{dQ@nT(N_OG;%~4#xg*6Jfai z$Fh9o&QqKdY8S4y6>l-%JD^{F^6T$f&8c3OvbLt!$Z=0lZFO=y^>ovk_3K}szHrs= z!L0o+*zC9b{QmuWW=+biSFcuutiEc&BJTa^`Of%r=g()>rQFLfnKXIwPFSg*CQ3o%GqY{9=Lz*+_@z|D>)u?U%q^K#j}~$@2_N9zTQ=052)3AKG`pp|RI=%(` z+{>`9^ZWPm^76%TzFHrZBKB7pNN_yZeY5u8je6attsgV%YDBE0Ro-3?=}u74=3wwq zW-#bwIAFrWaGAOsAyxh$T;N9Oy> zG#ozcoM|FeR9t*Ghv9vOe^&mh%!#*Fd+P3Cv#_(9_WO4=W6h%S+P7z2mqxMV=jL)g zm~3)({S8(pwI#MY3_}^Trgkyd?cB9%lF78mH6Hvjmz$5j`nt=;-rha@fO=K%yArFS zk`fWyMX&po&J+8ozyA1=SCR+j{O8tA>-Vg^c<|e|Z46=}T&)#1t}y($BKZ6F28ZfB zF?z@K*PAljk8S;R2ABfGehD)E(Ae_v^UsbP0l+XkKk|=x+ literal 0 HcmV?d00001 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 );