From 24b98b2dc08f09e246570bb48d5c8ebf064ade7c Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Thu, 2 Jan 2025 19:21:28 +0100 Subject: [PATCH 1/3] Implement procedures_definition --- src/dev/blocks/customblocks.cpp | 4 ++++ test/dev/blocks/custom_blocks_test.cpp | 27 +++++++++++++++++++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/dev/blocks/customblocks.cpp b/src/dev/blocks/customblocks.cpp index 4a1657b3..c8b195c5 100644 --- a/src/dev/blocks/customblocks.cpp +++ b/src/dev/blocks/customblocks.cpp @@ -1,5 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 +#include +#include + #include "customblocks.h" using namespace libscratchcpp; @@ -16,4 +19,5 @@ std::string CustomBlocks::description() const void CustomBlocks::registerBlocks(IEngine *engine) { + engine->addCompileFunction(this, "procedures_definition", [](Compiler *) -> CompilerValue * { return nullptr; }); } diff --git a/test/dev/blocks/custom_blocks_test.cpp b/test/dev/blocks/custom_blocks_test.cpp index a926f7db..97c344f5 100644 --- a/test/dev/blocks/custom_blocks_test.cpp +++ b/test/dev/blocks/custom_blocks_test.cpp @@ -1,15 +1,40 @@ +#include +#include +#include +#include #include #include "../common.h" #include "dev/blocks/customblocks.h" using namespace libscratchcpp; +using namespace libscratchcpp::test; class CustomBlocksTest : public testing::Test { public: - void SetUp() override { m_extension = std::make_unique(); } + void SetUp() override + { + m_extension = std::make_unique(); + m_engine = m_project.engine().get(); + m_extension->registerBlocks(m_engine); + } std::unique_ptr m_extension; + Project m_project; + IEngine *m_engine = nullptr; EngineMock m_engineMock; }; + +TEST_F(CustomBlocksTest, Definition) +{ + auto target = std::make_shared(); + ScriptBuilder builder(m_extension.get(), m_engine, target); + + builder.addBlock("procedures_definition"); + auto block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, target.get()); + compiler.compile(block); + ASSERT_TRUE(compiler.unsupportedBlocks().empty()); +} From b6ae58fd20e64390921bb0bb3f2cf6a9f444c2a3 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Fri, 3 Jan 2025 01:16:25 +0100 Subject: [PATCH 2/3] ScriptBuilder: Add support for building multiple scripts --- include/scratchcpp/dev/test/scriptbuilder.h | 11 +- src/dev/test/scriptbuilder.cpp | 114 ++++++++++++++------ test/dev/test_api/scriptbuilder_test.cpp | 38 ++++++- test/dev/test_api/testextension.cpp | 7 ++ test/dev/test_api/testextension.h | 1 + 5 files changed, 135 insertions(+), 36 deletions(-) diff --git a/include/scratchcpp/dev/test/scriptbuilder.h b/include/scratchcpp/dev/test/scriptbuilder.h index e2045a2e..a01db6b8 100644 --- a/include/scratchcpp/dev/test/scriptbuilder.h +++ b/include/scratchcpp/dev/test/scriptbuilder.h @@ -25,11 +25,12 @@ class ScriptBuilderPrivate; class LIBSCRATCHCPP_EXPORT ScriptBuilder { public: - ScriptBuilder(IExtension *extension, IEngine *engine, std::shared_ptr target); + ScriptBuilder(IExtension *extension, IEngine *engine, std::shared_ptr target, bool createHatBlock = true); ScriptBuilder(const ScriptBuilder &) = delete; ~ScriptBuilder(); + void addBlock(std::shared_ptr block); void addBlock(const std::string &opcode); void captureBlockReturnValue(); @@ -53,9 +54,15 @@ class LIBSCRATCHCPP_EXPORT ScriptBuilder List *capturedValues() const; + static void buildMultiple(const std::vector &builders); + private: - void addBlock(std::shared_ptr block); + void addBlockToList(std::shared_ptr block); void build(std::shared_ptr target); + std::string nextId(); + + static void addBlocksToTarget(Target *target, const std::vector> &blocks); + static void addTargetToEngine(IEngine *engine, std::shared_ptr target); spimpl::unique_impl_ptr impl; }; diff --git a/src/dev/test/scriptbuilder.cpp b/src/dev/test/scriptbuilder.cpp index 641b3d0e..363aed7e 100644 --- a/src/dev/test/scriptbuilder.cpp +++ b/src/dev/test/scriptbuilder.cpp @@ -16,32 +16,31 @@ using namespace libscratchcpp; using namespace libscratchcpp::test; -static std::unordered_map> captureLists; +// TODO: Add support for return values captures when building multiple scripts +static std::unordered_map> captureLists; +static ScriptBuilder *currentScriptBuilder = nullptr; /*! Constructs ScriptBuilder. */ -ScriptBuilder::ScriptBuilder(IExtension *extension, IEngine *engine, std::shared_ptr target) : +ScriptBuilder::ScriptBuilder(IExtension *extension, IEngine *engine, std::shared_ptr target, bool createHatBlock) : impl(spimpl::make_unique_impl(engine, target)) { // Create capture list - if (captureLists.find(engine) != captureLists.cend()) { - std::cerr << "error: only one ScriptBuilder can be created for each engine" << std::endl; - return; - } - - captureLists[engine] = std::make_shared("", ""); + captureLists[this] = std::make_shared("", ""); // Add start hat block - auto block = std::make_shared(std::to_string(impl->blockId++), "script_builder_init"); - engine->addCompileFunction(extension, block->opcode(), [](Compiler *compiler) -> CompilerValue * { - compiler->engine()->addGreenFlagScript(compiler->block()); - return nullptr; - }); - addBlock(block); + if (createHatBlock) { + auto block = std::make_shared(nextId(), "script_builder_init"); + engine->addCompileFunction(extension, block->opcode(), [](Compiler *compiler) -> CompilerValue * { + compiler->engine()->addGreenFlagScript(compiler->block()); + return nullptr; + }); + addBlockToList(block); + } // Add compile function for return value capture block engine->addCompileFunction(extension, "script_builder_capture", [](Compiler *compiler) -> CompilerValue * { CompilerValue *input = compiler->addInput("VALUE"); - compiler->createListAppend(captureLists[compiler->engine()].get(), input); + compiler->createListAppend(captureLists[currentScriptBuilder].get(), input); return nullptr; }); } @@ -49,14 +48,21 @@ ScriptBuilder::ScriptBuilder(IExtension *extension, IEngine *engine, std::shared /*! Destroys ScriptBuilder. */ ScriptBuilder::~ScriptBuilder() { - captureLists.erase(impl->engine); + captureLists.erase(this); +} + +/*! Adds the given block to the script. */ +void ScriptBuilder::addBlock(std::shared_ptr block) +{ + block->setId(nextId()); + impl->lastBlock = block; + addBlockToList(block); } /*! Adds a block with the given opcode to the script. */ void ScriptBuilder::addBlock(const std::string &opcode) { - impl->lastBlock = std::make_shared(std::to_string(impl->blockId++), opcode); - addBlock(impl->lastBlock); + addBlock(std::make_shared("", opcode)); } /*! Captures the return value of the created reporter block. It can be retrieved using capturedValues() later. */ @@ -101,7 +107,7 @@ void ScriptBuilder::addObscuredInput(const std::string &name, std::shared_ptrsetParent(impl->lastBlock); while (block) { - block->setId(std::to_string(impl->blockId++)); + block->setId(nextId()); impl->inputBlocks.push_back(block); auto parent = block->parent(); @@ -128,7 +134,7 @@ void ScriptBuilder::addNullObscuredInput(const std::string &name) return; auto input = std::make_shared(name, Input::Type::ObscuredShadow); - auto block = std::make_shared(std::to_string(impl->blockId++), ""); + auto block = std::make_shared(nextId(), ""); block->setCompileFunction([](Compiler *compiler) -> CompilerValue * { return compiler->addConstValue(Value()); }); input->setValueBlock(block); impl->inputBlocks.push_back(block); @@ -144,7 +150,7 @@ void ScriptBuilder::addDropdownInput(const std::string &name, const std::string auto input = std::make_shared(name, Input::Type::Shadow); impl->lastBlock->addInput(input); - auto menu = std::make_shared(std::to_string(impl->blockId++), impl->lastBlock->opcode() + "_menu"); + auto menu = std::make_shared(nextId(), impl->lastBlock->opcode() + "_menu"); menu->setShadow(true); impl->inputBlocks.push_back(menu); input->setValueBlock(menu); @@ -170,7 +176,7 @@ void ScriptBuilder::addEntityInput(const std::string &name, const std::string &e return; if (std::find(impl->entities.begin(), impl->entities.end(), entity) == impl->entities.end()) { - entity->setId(std::to_string(impl->blockId++)); + entity->setId(nextId()); impl->entities.push_back(entity); } @@ -188,7 +194,7 @@ void ScriptBuilder::addEntityField(const std::string &name, std::shared_ptrentities.begin(), impl->entities.end(), entity) == impl->entities.end()) { - entity->setId(std::to_string(impl->blockId++)); + entity->setId(nextId()); impl->entities.push_back(entity); } @@ -218,6 +224,10 @@ std::shared_ptr ScriptBuilder::currentBlock() target->addList(list); build(target); + + std::vector> targets = impl->engine->targets(); + targets.erase(std::remove(targets.begin(), targets.end(), target), targets.end()); + impl->engine->setTargets(targets); } return impl->lastBlock; @@ -256,10 +266,31 @@ void ScriptBuilder::run() /*! Returns the list of captured block return values. */ List *ScriptBuilder::capturedValues() const { - return captureLists[impl->engine].get(); + return captureLists[this].get(); } -void ScriptBuilder::addBlock(std::shared_ptr block) +/*! + * Builds multiple scripts using the given script builders. + * \note Using run() on any of the script builders will result in all scripts without a custom hat block being called. Use this only with a single when flag clicked block. + * \note Return value capturing is not supported when building multiple scripts. + */ +void ScriptBuilder::buildMultiple(const std::vector &builders) +{ + std::unordered_set engines; + + for (ScriptBuilder *builder : builders) { + auto target = builder->impl->target; + addBlocksToTarget(target.get(), builder->impl->blocks); + addBlocksToTarget(target.get(), builder->impl->inputBlocks); + addTargetToEngine(builder->impl->engine, target); + engines.insert(builder->impl->engine); + } + + for (IEngine *engine : engines) + engine->compile(); +} + +void ScriptBuilder::addBlockToList(std::shared_ptr block) { if (!impl->blocks.empty()) { auto lastBlock = impl->blocks.back(); @@ -272,20 +303,37 @@ void ScriptBuilder::addBlock(std::shared_ptr block) void ScriptBuilder::build(std::shared_ptr target) { - if (target->blocks().empty()) { - for (auto block : impl->blocks) - target->addBlock(block); + currentScriptBuilder = this; + + addBlocksToTarget(target.get(), impl->blocks); + addBlocksToTarget(target.get(), impl->inputBlocks); + addTargetToEngine(impl->engine, target); + + impl->engine->compile(); + currentScriptBuilder = nullptr; +} + +std::string ScriptBuilder::nextId() +{ + return std::to_string((uintptr_t)this) + '.' + std::to_string(impl->blockId++); +} + +void ScriptBuilder::addBlocksToTarget(Target *target, const std::vector> &blocks) +{ + auto targetBlocks = target->blocks(); - for (auto block : impl->inputBlocks) + for (auto block : blocks) { + if (std::find(targetBlocks.begin(), targetBlocks.end(), block) == targetBlocks.end()) target->addBlock(block); } +} - std::vector> targets = impl->engine->targets(); +void ScriptBuilder::addTargetToEngine(IEngine *engine, std::shared_ptr target) +{ + std::vector> targets = engine->targets(); if (std::find(targets.begin(), targets.end(), target) == targets.end()) { targets.push_back(target); - impl->engine->setTargets({ target }); + engine->setTargets(targets); } - - impl->engine->compile(); } diff --git a/test/dev/test_api/scriptbuilder_test.cpp b/test/dev/test_api/scriptbuilder_test.cpp index 2239832a..1182b18e 100644 --- a/test/dev/test_api/scriptbuilder_test.cpp +++ b/test/dev/test_api/scriptbuilder_test.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #include @@ -43,11 +44,18 @@ TEST_F(ScriptBuilderTest, AddBlock) ASSERT_EQ(block->opcode(), "test_simple"); ASSERT_TRUE(block->compileFunction()); + block = std::make_shared("", "test_simple"); + m_builder->addBlock(block); + block = m_builder->currentBlock(); + ASSERT_TRUE(block); + ASSERT_EQ(block->opcode(), "test_simple"); + ASSERT_TRUE(block->compileFunction()); + m_builder->build(); testing::internal::CaptureStdout(); m_builder->run(); - ASSERT_EQ(testing::internal::GetCapturedStdout(), "test\n"); + ASSERT_EQ(testing::internal::GetCapturedStdout(), "test\ntest\n"); } TEST_F(ScriptBuilderTest, AddValueInput) @@ -271,3 +279,31 @@ TEST_F(ScriptBuilderTest, CaptureBlockReturnValue) ASSERT_EQ(str, "test"); ASSERT_EQ(value_toDouble(&values->operator[](1)), -93.4); } + +TEST_F(ScriptBuilderTest, MultipleScripts) +{ + ScriptBuilder builder1(&m_extension, m_engine, m_target, false); + builder1.addBlock("test_click_hat"); + builder1.addBlock("test_simple"); + + ScriptBuilder builder2(&m_extension, m_engine, m_target); + builder2.addBlock("test_print"); + builder2.addValueInput("STRING", "Hello world"); + + Project project; + IEngine *engine = project.engine().get(); + m_extension.registerBlocks(engine); + auto target = std::make_shared(); + ScriptBuilder builder3(&m_extension, engine, target); + builder3.addBlock("test_simple"); + + ScriptBuilder::buildMultiple({ &builder1, &builder2, &builder3 }); + + testing::internal::CaptureStdout(); + builder2.run(); + ASSERT_EQ(testing::internal::GetCapturedStdout(), "Hello world\n"); + + testing::internal::CaptureStdout(); + builder3.run(); + ASSERT_EQ(testing::internal::GetCapturedStdout(), "test\n"); +} diff --git a/test/dev/test_api/testextension.cpp b/test/dev/test_api/testextension.cpp index 7e6ef22d..c637e522 100644 --- a/test/dev/test_api/testextension.cpp +++ b/test/dev/test_api/testextension.cpp @@ -30,6 +30,7 @@ void TestExtension::registerBlocks(IEngine *engine) engine->addCompileFunction(this, "test_teststr", &compileTestStr); engine->addCompileFunction(this, "test_input", &compileInput); engine->addCompileFunction(this, "test_substack", &compileSubstack); + engine->addCompileFunction(this, "test_click_hat", &compileClickHat); } CompilerValue *TestExtension::compileSimple(Compiler *compiler) @@ -77,6 +78,12 @@ CompilerValue *TestExtension::compileSubstack(Compiler *compiler) return nullptr; } +CompilerValue *TestExtension::compileClickHat(Compiler *compiler) +{ + compiler->engine()->addTargetClickScript(compiler->block()); + return nullptr; +} + extern "C" void test_simple() { std::cout << "test" << std::endl; diff --git a/test/dev/test_api/testextension.h b/test/dev/test_api/testextension.h index 6c88ad25..e69bbcdd 100644 --- a/test/dev/test_api/testextension.h +++ b/test/dev/test_api/testextension.h @@ -21,6 +21,7 @@ class TestExtension : public IExtension static CompilerValue *compileTestStr(Compiler *compiler); static CompilerValue *compileInput(Compiler *compiler); static CompilerValue *compileSubstack(Compiler *compiler); + static CompilerValue *compileClickHat(Compiler *compiler); }; } // namespace libscratchcpp From 8e32dea74823fe5f062467b68d48f0d895b7b72b Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Fri, 3 Jan 2025 01:18:19 +0100 Subject: [PATCH 3/3] Implement remaining procedure blocks --- src/dev/blocks/customblocks.cpp | 32 +++++++++++++ src/dev/blocks/customblocks.h | 4 ++ test/dev/blocks/CMakeLists.txt | 1 + test/dev/blocks/custom_blocks_test.cpp | 63 ++++++++++++++++++++++++++ 4 files changed, 100 insertions(+) diff --git a/src/dev/blocks/customblocks.cpp b/src/dev/blocks/customblocks.cpp index c8b195c5..e78048bc 100644 --- a/src/dev/blocks/customblocks.cpp +++ b/src/dev/blocks/customblocks.cpp @@ -2,6 +2,9 @@ #include #include +#include +#include +#include #include "customblocks.h" @@ -20,4 +23,33 @@ std::string CustomBlocks::description() const void CustomBlocks::registerBlocks(IEngine *engine) { engine->addCompileFunction(this, "procedures_definition", [](Compiler *) -> CompilerValue * { return nullptr; }); + engine->addCompileFunction(this, "procedures_call", &compileCall); + engine->addCompileFunction(this, "argument_reporter_boolean", &compileArgument); + engine->addCompileFunction(this, "argument_reporter_string_number", &compileArgument); +} + +CompilerValue *CustomBlocks::compileCall(Compiler *compiler) +{ + auto block = compiler->block(); + auto prototype = block->mutationPrototype(); + const std::vector &procedureArgs = prototype->argumentIds(); + Compiler::Args args; + + for (size_t i = 0; i < procedureArgs.size(); i++) { + auto index = block->findInput(procedureArgs[i]); + + if (index == -1) + args.push_back(compiler->addConstValue(Value())); + else + args.push_back(compiler->addInput(block->inputAt(index).get())); + } + + compiler->createProcedureCall(compiler->block()->mutationPrototype(), args); + return nullptr; +} + +CompilerValue *CustomBlocks::compileArgument(Compiler *compiler) +{ + const std::string &argName = compiler->field("VALUE")->value().toString(); + return compiler->addProcedureArgument(argName); } diff --git a/src/dev/blocks/customblocks.h b/src/dev/blocks/customblocks.h index 433afa36..55572ba9 100644 --- a/src/dev/blocks/customblocks.h +++ b/src/dev/blocks/customblocks.h @@ -14,6 +14,10 @@ class CustomBlocks : public IExtension std::string description() const override; void registerBlocks(IEngine *engine) override; + + private: + static CompilerValue *compileCall(Compiler *compiler); + static CompilerValue *compileArgument(Compiler *compiler); }; } // namespace libscratchcpp diff --git a/test/dev/blocks/CMakeLists.txt b/test/dev/blocks/CMakeLists.txt index 5239c047..e8694da9 100644 --- a/test/dev/blocks/CMakeLists.txt +++ b/test/dev/blocks/CMakeLists.txt @@ -188,6 +188,7 @@ if (LIBSCRATCHCPP_ENABLE_CUSTOM_BLOCKS) GTest::gmock_main scratchcpp scratchcpp_mocks + block_test_deps ) gtest_discover_tests(custom_blocks_test) diff --git a/test/dev/blocks/custom_blocks_test.cpp b/test/dev/blocks/custom_blocks_test.cpp index 97c344f5..515756f5 100644 --- a/test/dev/blocks/custom_blocks_test.cpp +++ b/test/dev/blocks/custom_blocks_test.cpp @@ -1,11 +1,17 @@ #include #include #include +#include +#include #include +#include +#include +#include #include #include "../common.h" #include "dev/blocks/customblocks.h" +#include "util.h" using namespace libscratchcpp; using namespace libscratchcpp::test; @@ -18,6 +24,7 @@ class CustomBlocksTest : public testing::Test m_extension = std::make_unique(); m_engine = m_project.engine().get(); m_extension->registerBlocks(m_engine); + registerBlocks(m_engine, m_extension.get()); } std::unique_ptr m_extension; @@ -38,3 +45,59 @@ TEST_F(CustomBlocksTest, Definition) compiler.compile(block); ASSERT_TRUE(compiler.unsupportedBlocks().empty()); } + +TEST_F(CustomBlocksTest, CallWithArguments) +{ + const std::string procCode = "procedure %s %b"; + std::vector argumentIds = { "a", "b" }; + std::vector argumentNames = { "string or number", "boolean" }; + auto target = std::make_shared(); + + // Create definition + ScriptBuilder builder1(m_extension.get(), m_engine, target, false); + builder1.addBlock("procedures_prototype"); + auto prototypeBlock = builder1.takeBlock(); + BlockPrototype *prototype = prototypeBlock->mutationPrototype(); + prototype->setProcCode(procCode); + prototype->setArgumentIds(argumentIds); + prototype->setArgumentNames(argumentNames); + + builder1.addBlock("procedures_definition"); + builder1.addObscuredInput("custom_block", prototypeBlock); + builder1.currentBlock(); + + // Print first arg + builder1.addBlock("argument_reporter_string_number"); + builder1.addDropdownField("VALUE", "string or number"); + auto argBlock = builder1.takeBlock(); + + builder1.addBlock("test_print"); + builder1.addObscuredInput("STRING", argBlock); + + // Print second arg + builder1.addBlock("argument_reporter_boolean"); + builder1.addDropdownField("VALUE", "boolean"); + argBlock = builder1.takeBlock(); + + builder1.addBlock("test_print"); + builder1.addObscuredInput("STRING", argBlock); + + builder1.build(); + + // Call the procedure + ScriptBuilder builder2(m_extension.get(), m_engine, target); + auto block = std::make_shared("", "procedures_call"); + prototype = block->mutationPrototype(); + prototype->setProcCode(procCode); + prototype->setArgumentIds(argumentIds); + prototype->setArgumentNames(argumentNames); + builder2.addBlock(block); + builder2.addValueInput("a", "Hello world"); + builder2.addValueInput("b", true); + + ScriptBuilder::buildMultiple({ &builder1, &builder2 }); + + testing::internal::CaptureStdout(); + builder2.run(); + ASSERT_EQ(testing::internal::GetCapturedStdout(), "Hello world\ntrue\n"); +}