From 9ea8a1f01915787f60c30cd59bf9237e39f9f08d Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Thu, 7 Aug 2025 22:23:31 +0200 Subject: [PATCH 01/20] Implement sensing_touchingobject block --- src/blocks/sensingblocks.cpp | 78 ++++++ src/blocks/sensingblocks.h | 3 + test/blocks/CMakeLists.txt | 1 + test/blocks/sensing_blocks_test.cpp | 356 +++++++++++++++++++++++++++- 4 files changed, 437 insertions(+), 1 deletion(-) diff --git a/src/blocks/sensingblocks.cpp b/src/blocks/sensingblocks.cpp index 8e3b18cac..151355e71 100644 --- a/src/blocks/sensingblocks.cpp +++ b/src/blocks/sensingblocks.cpp @@ -1,5 +1,15 @@ // SPDX-License-Identifier: Apache-2.0 +#include +#include +#include +#include +#include +#include +#include +#include +#include + #include "sensingblocks.h" using namespace libscratchcpp; @@ -21,4 +31,72 @@ Rgb SensingBlocks::color() const void SensingBlocks::registerBlocks(IEngine *engine) { + engine->addCompileFunction(this, "sensing_touchingobject", &compileTouchingObject); +} + +CompilerValue *SensingBlocks::compileTouchingObject(Compiler *compiler) +{ + IEngine *engine = compiler->engine(); + Input *input = compiler->input("TOUCHINGOBJECTMENU"); + + if (input->pointsToDropdownMenu()) { + std::string value = input->selectedMenuItem(); + + if (value == "_mouse_") + return compiler->addTargetFunctionCall("sensing_touching_mouse", Compiler::StaticType::Bool); + else if (value == "_edge_") + return compiler->addTargetFunctionCall("sensing_touching_edge", Compiler::StaticType::Bool); + else if (value != "_stage_") { + Target *target = engine->targetAt(engine->findTarget(value)); + + if (target && !target->isStage()) { + CompilerValue *targetPtr = compiler->addConstValue(target); + return compiler->addTargetFunctionCall("sensing_touching_sprite", Compiler::StaticType::Bool, { Compiler::StaticType::Pointer }, { targetPtr }); + } + } + } else { + CompilerValue *object = compiler->addInput(input); + return compiler->addTargetFunctionCall("sensing_touchingobject", Compiler::StaticType::Bool, { Compiler::StaticType::String }, { object }); + } + + return compiler->addConstValue(false); +} + +extern "C" bool sensing_touching_mouse(Target *target) +{ + IEngine *engine = target->engine(); + return target->touchingPoint(engine->mouseX(), engine->mouseY()); +} + +extern "C" bool sensing_touching_edge(Target *target) +{ + return target->touchingEdge(); +} + +extern "C" bool sensing_touching_sprite(Target *target, Sprite *sprite) +{ + return target->touchingSprite(sprite); +} + +extern "C" bool sensing_touchingobject(Target *target, const StringPtr *object) +{ + static const StringPtr MOUSE_STR("_mouse_"); + static const StringPtr EDGE_STR("_edge_"); + static const StringPtr STAGE_STR("_stage_"); + + if (string_compare_case_sensitive(object, &MOUSE_STR) == 0) + return sensing_touching_mouse(target); + else if (string_compare_case_sensitive(object, &EDGE_STR) == 0) + return sensing_touching_edge(target); + else if (string_compare_case_sensitive(object, &STAGE_STR) != 0) { + IEngine *engine = target->engine(); + // TODO: Use UTF-16 in engine + std::string u8name = utf8::utf16to8(std::u16string(object->data)); + Target *objTarget = engine->targetAt(engine->findTarget(u8name)); + + if (objTarget) + return sensing_touching_sprite(target, static_cast(objTarget)); + } + + return false; } diff --git a/src/blocks/sensingblocks.h b/src/blocks/sensingblocks.h index 9a0f367f5..dd0c77c98 100644 --- a/src/blocks/sensingblocks.h +++ b/src/blocks/sensingblocks.h @@ -15,6 +15,9 @@ class SensingBlocks : public IExtension Rgb color() const override; void registerBlocks(IEngine *engine) override; + + private: + static CompilerValue *compileTouchingObject(Compiler *compiler); }; } // namespace libscratchcpp diff --git a/test/blocks/CMakeLists.txt b/test/blocks/CMakeLists.txt index e8694da9a..2a8bd3800 100644 --- a/test/blocks/CMakeLists.txt +++ b/test/blocks/CMakeLists.txt @@ -114,6 +114,7 @@ if (LIBSCRATCHCPP_ENABLE_SENSING_BLOCKS) GTest::gmock_main scratchcpp scratchcpp_mocks + block_test_deps ) gtest_discover_tests(sensing_blocks_test) diff --git a/test/blocks/sensing_blocks_test.cpp b/test/blocks/sensing_blocks_test.cpp index d1730f9c6..267c4ef35 100644 --- a/test/blocks/sensing_blocks_test.cpp +++ b/test/blocks/sensing_blocks_test.cpp @@ -1,15 +1,369 @@ +#include +#include +#include +#include +#include +#include +#include +#include #include +#include #include "../common.h" #include "blocks/sensingblocks.h" +#include "util.h" using namespace libscratchcpp; +using namespace libscratchcpp::test; + +using ::testing::Return; class SensingBlocksTest : 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); + registerBlocks(m_engine, m_extension.get()); + } std::unique_ptr m_extension; + Project m_project; + IEngine *m_engine = nullptr; EngineMock m_engineMock; }; + +TEST_F(SensingBlocksTest, TouchingObject_Sprite_CompileTime) +{ + auto targetMock = std::make_shared(); + targetMock->setEngine(&m_engineMock); + + Sprite sprite; + + ScriptBuilder builder(m_extension.get(), m_engine, targetMock); + builder.addBlock("sensing_touchingobject"); + builder.addDropdownInput("TOUCHINGOBJECTMENU", "Sprite2"); + Block *block = builder.currentBlock(); + + EXPECT_CALL(m_engineMock, findTarget("Sprite2")).WillOnce(Return(3)); + EXPECT_CALL(m_engineMock, targetAt(3)).WillOnce(Return(&sprite)); + Compiler compiler(&m_engineMock, targetMock.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(targetMock.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(targetMock.get(), &m_engineMock, &script); + + EXPECT_CALL(*targetMock, touchingClones).WillOnce(Return(true)); + ValueData value = thread.runReporter(); + ASSERT_TRUE(value_toBool(&value)); + value_free(&value); + + EXPECT_CALL(*targetMock, touchingClones).WillOnce(Return(false)); + value = thread.runReporter(); + ASSERT_FALSE(value_toBool(&value)); + value_free(&value); +} + +TEST_F(SensingBlocksTest, TouchingObject_Sprite_Runtime) +{ + auto targetMock = std::make_shared(); + targetMock->setEngine(&m_engineMock); + + Sprite sprite; + + ScriptBuilder builder(m_extension.get(), m_engine, targetMock); + + builder.addBlock("test_const_string"); + builder.addValueInput("STRING", "Sprite2"); + auto valueBlock = builder.takeBlock(); + + builder.addBlock("sensing_touchingobject"); + builder.addObscuredInput("TOUCHINGOBJECTMENU", valueBlock); + Block *block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, targetMock.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(targetMock.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(targetMock.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, findTarget("Sprite2")).WillRepeatedly(Return(5)); + EXPECT_CALL(m_engineMock, targetAt(5)).WillRepeatedly(Return(&sprite)); + + EXPECT_CALL(*targetMock, touchingClones).WillOnce(Return(true)); + ValueData value = thread.runReporter(); + ASSERT_TRUE(value_toBool(&value)); + value_free(&value); + + EXPECT_CALL(*targetMock, touchingClones).WillOnce(Return(false)); + value = thread.runReporter(); + ASSERT_FALSE(value_toBool(&value)); + value_free(&value); +} + +TEST_F(SensingBlocksTest, TouchingObject_Stage_CompileTime) +{ + auto targetMock = std::make_shared(); + targetMock->setEngine(&m_engineMock); + + Stage stage; + + ScriptBuilder builder(m_extension.get(), m_engine, targetMock); + builder.addBlock("sensing_touchingobject"); + builder.addDropdownInput("TOUCHINGOBJECTMENU", "_stage_"); + Block *block = builder.currentBlock(); + + EXPECT_CALL(m_engineMock, findTarget("_stage_")).WillRepeatedly(Return(3)); + EXPECT_CALL(m_engineMock, targetAt(3)).WillRepeatedly(Return(&stage)); + Compiler compiler(&m_engineMock, targetMock.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(targetMock.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(targetMock.get(), &m_engineMock, &script); + + EXPECT_CALL(*targetMock, touchingClones).Times(0); + ValueData value = thread.runReporter(); + ASSERT_FALSE(value_toBool(&value)); + value_free(&value); +} + +TEST_F(SensingBlocksTest, TouchingObject_Stage_Runtime) +{ + auto targetMock = std::make_shared(); + targetMock->setEngine(&m_engineMock); + + Stage stage; + + ScriptBuilder builder(m_extension.get(), m_engine, targetMock); + + builder.addBlock("test_const_string"); + builder.addValueInput("STRING", "_stage_"); + auto valueBlock = builder.takeBlock(); + + builder.addBlock("sensing_touchingobject"); + builder.addObscuredInput("TOUCHINGOBJECTMENU", valueBlock); + Block *block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, targetMock.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(targetMock.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(targetMock.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, findTarget("_stage_")).WillRepeatedly(Return(5)); + EXPECT_CALL(m_engineMock, targetAt(5)).WillRepeatedly(Return(&stage)); + + EXPECT_CALL(*targetMock, touchingClones).Times(0); + ValueData value = thread.runReporter(); + ASSERT_FALSE(value_toBool(&value)); + value_free(&value); +} + +TEST_F(SensingBlocksTest, TouchingObject_Mouse_CompileTime) +{ + auto targetMock = std::make_shared(); + targetMock->setEngine(&m_engineMock); + + Sprite sprite; + + ScriptBuilder builder(m_extension.get(), m_engine, targetMock); + builder.addBlock("sensing_touchingobject"); + builder.addDropdownInput("TOUCHINGOBJECTMENU", "_mouse_"); + Block *block = builder.currentBlock(); + + EXPECT_CALL(m_engineMock, findTarget).Times(0); + EXPECT_CALL(m_engineMock, targetAt).Times(0); + Compiler compiler(&m_engineMock, targetMock.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(targetMock.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(targetMock.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, mouseX).WillOnce(Return(56.2)); + EXPECT_CALL(m_engineMock, mouseY).WillOnce(Return(-89.5)); + EXPECT_CALL(*targetMock, touchingPoint(56.2, -89.5)).WillOnce(Return(true)); + ValueData value = thread.runReporter(); + ASSERT_TRUE(value_toBool(&value)); + value_free(&value); + + EXPECT_CALL(m_engineMock, mouseX).WillOnce(Return(-12.7)); + EXPECT_CALL(m_engineMock, mouseY).WillOnce(Return(2.2)); + EXPECT_CALL(*targetMock, touchingPoint(-12.7, 2.2)).WillOnce(Return(false)); + value = thread.runReporter(); + ASSERT_FALSE(value_toBool(&value)); + value_free(&value); +} + +TEST_F(SensingBlocksTest, TouchingObject_Mouse_Runtime) +{ + auto targetMock = std::make_shared(); + targetMock->setEngine(&m_engineMock); + + Sprite sprite; + + ScriptBuilder builder(m_extension.get(), m_engine, targetMock); + + builder.addBlock("test_const_string"); + builder.addValueInput("STRING", "_mouse_"); + auto valueBlock = builder.takeBlock(); + + builder.addBlock("sensing_touchingobject"); + builder.addObscuredInput("TOUCHINGOBJECTMENU", valueBlock); + Block *block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, targetMock.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(targetMock.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(targetMock.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, findTarget).Times(0); + EXPECT_CALL(m_engineMock, targetAt).Times(0); + + EXPECT_CALL(m_engineMock, mouseX).WillOnce(Return(56.2)); + EXPECT_CALL(m_engineMock, mouseY).WillOnce(Return(-89.5)); + EXPECT_CALL(*targetMock, touchingPoint(56.2, -89.5)).WillOnce(Return(true)); + ValueData value = thread.runReporter(); + ASSERT_TRUE(value_toBool(&value)); + value_free(&value); + + EXPECT_CALL(m_engineMock, mouseX).WillOnce(Return(-12.7)); + EXPECT_CALL(m_engineMock, mouseY).WillOnce(Return(2.2)); + EXPECT_CALL(*targetMock, touchingPoint(-12.7, 2.2)).WillOnce(Return(false)); + value = thread.runReporter(); + ASSERT_FALSE(value_toBool(&value)); + value_free(&value); +} + +TEST_F(SensingBlocksTest, TouchingObject_Edge_CompileTime) +{ + auto targetMock = std::make_shared(); + targetMock->setEngine(&m_engineMock); + + Sprite sprite; + + ScriptBuilder builder(m_extension.get(), m_engine, targetMock); + builder.addBlock("sensing_touchingobject"); + builder.addDropdownInput("TOUCHINGOBJECTMENU", "_edge_"); + Block *block = builder.currentBlock(); + + EXPECT_CALL(m_engineMock, findTarget).Times(0); + EXPECT_CALL(m_engineMock, targetAt).Times(0); + Compiler compiler(&m_engineMock, targetMock.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(targetMock.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(targetMock.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, stageWidth()).WillOnce(Return(2)); + EXPECT_CALL(m_engineMock, stageHeight()).WillOnce(Return(2)); + EXPECT_CALL(*targetMock, boundingRect()).WillOnce(Return(Rect(-5, 5, 5, -5))); + ValueData value = thread.runReporter(); + ASSERT_TRUE(value_toBool(&value)); + value_free(&value); + + EXPECT_CALL(m_engineMock, stageWidth()).WillOnce(Return(10)); + EXPECT_CALL(m_engineMock, stageHeight()).WillOnce(Return(10)); + EXPECT_CALL(*targetMock, boundingRect()).WillOnce(Return(Rect(-5, 5, 5, -5))); + value = thread.runReporter(); + ASSERT_FALSE(value_toBool(&value)); + value_free(&value); +} + +TEST_F(SensingBlocksTest, TouchingObject_Edge_Runtime) +{ + auto targetMock = std::make_shared(); + targetMock->setEngine(&m_engineMock); + + Sprite sprite; + + ScriptBuilder builder(m_extension.get(), m_engine, targetMock); + + builder.addBlock("test_const_string"); + builder.addValueInput("STRING", "_edge_"); + auto valueBlock = builder.takeBlock(); + + builder.addBlock("sensing_touchingobject"); + builder.addObscuredInput("TOUCHINGOBJECTMENU", valueBlock); + Block *block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, targetMock.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(targetMock.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(targetMock.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, findTarget).Times(0); + EXPECT_CALL(m_engineMock, targetAt).Times(0); + + EXPECT_CALL(m_engineMock, stageWidth()).WillOnce(Return(2)); + EXPECT_CALL(m_engineMock, stageHeight()).WillOnce(Return(2)); + EXPECT_CALL(*targetMock, boundingRect()).WillOnce(Return(Rect(-5, 5, 5, -5))); + ValueData value = thread.runReporter(); + ASSERT_TRUE(value_toBool(&value)); + value_free(&value); + + EXPECT_CALL(m_engineMock, stageWidth()).WillOnce(Return(10)); + EXPECT_CALL(m_engineMock, stageHeight()).WillOnce(Return(10)); + EXPECT_CALL(*targetMock, boundingRect()).WillOnce(Return(Rect(-5, 5, 5, -5))); + value = thread.runReporter(); + ASSERT_FALSE(value_toBool(&value)); + value_free(&value); +} + +TEST_F(SensingBlocksTest, TouchingObject_Invalid_CompileTime) +{ + auto targetMock = std::make_shared(); + targetMock->setEngine(&m_engineMock); + + Sprite sprite; + + ScriptBuilder builder(m_extension.get(), m_engine, targetMock); + builder.addBlock("sensing_touchingobject"); + builder.addDropdownInput("TOUCHINGOBJECTMENU", "test"); + Block *block = builder.currentBlock(); + + EXPECT_CALL(m_engineMock, findTarget("test")).WillOnce(Return(-1)); + EXPECT_CALL(m_engineMock, targetAt).WillRepeatedly(Return(nullptr)); + Compiler compiler(&m_engineMock, targetMock.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(targetMock.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(targetMock.get(), &m_engineMock, &script); + + ValueData value = thread.runReporter(); + ASSERT_FALSE(value_toBool(&value)); + value_free(&value); +} + +TEST_F(SensingBlocksTest, TouchingObject_Invalid_Runtime) +{ + auto targetMock = std::make_shared(); + targetMock->setEngine(&m_engineMock); + + Sprite sprite; + + ScriptBuilder builder(m_extension.get(), m_engine, targetMock); + + builder.addBlock("test_const_string"); + builder.addValueInput("STRING", "test"); + auto valueBlock = builder.takeBlock(); + + builder.addBlock("sensing_touchingobject"); + builder.addObscuredInput("TOUCHINGOBJECTMENU", valueBlock); + Block *block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, targetMock.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(targetMock.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(targetMock.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, findTarget("test")).WillRepeatedly(Return(-1)); + EXPECT_CALL(m_engineMock, targetAt).WillRepeatedly(Return(nullptr)); + ValueData value = thread.runReporter(); + ASSERT_FALSE(value_toBool(&value)); + value_free(&value); +} From 369ad2b3652a40fcd00c7d179f349b50be1af858 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sat, 9 Aug 2025 09:39:35 +0200 Subject: [PATCH 02/20] Implement sensing_touchingcolor block --- src/blocks/sensingblocks.cpp | 12 +++++++ src/blocks/sensingblocks.h | 1 + test/blocks/sensing_blocks_test.cpp | 54 +++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+) diff --git a/src/blocks/sensingblocks.cpp b/src/blocks/sensingblocks.cpp index 151355e71..29d2c8b97 100644 --- a/src/blocks/sensingblocks.cpp +++ b/src/blocks/sensingblocks.cpp @@ -32,6 +32,7 @@ Rgb SensingBlocks::color() const void SensingBlocks::registerBlocks(IEngine *engine) { engine->addCompileFunction(this, "sensing_touchingobject", &compileTouchingObject); + engine->addCompileFunction(this, "sensing_touchingcolor", &compileTouchingColor); } CompilerValue *SensingBlocks::compileTouchingObject(Compiler *compiler) @@ -62,6 +63,12 @@ CompilerValue *SensingBlocks::compileTouchingObject(Compiler *compiler) return compiler->addConstValue(false); } +CompilerValue *SensingBlocks::compileTouchingColor(Compiler *compiler) +{ + CompilerValue *color = compiler->addInput("COLOR"); + return compiler->addTargetFunctionCall("sensing_touchingcolor", Compiler::StaticType::Bool, { Compiler::StaticType::Unknown }, { color }); +} + extern "C" bool sensing_touching_mouse(Target *target) { IEngine *engine = target->engine(); @@ -100,3 +107,8 @@ extern "C" bool sensing_touchingobject(Target *target, const StringPtr *object) return false; } + +extern "C" bool sensing_touchingcolor(Target *target, const ValueData *color) +{ + return target->touchingColor(value_toRgba(color)); +} diff --git a/src/blocks/sensingblocks.h b/src/blocks/sensingblocks.h index dd0c77c98..a6fd2f4bc 100644 --- a/src/blocks/sensingblocks.h +++ b/src/blocks/sensingblocks.h @@ -18,6 +18,7 @@ class SensingBlocks : public IExtension private: static CompilerValue *compileTouchingObject(Compiler *compiler); + static CompilerValue *compileTouchingColor(Compiler *compiler); }; } // namespace libscratchcpp diff --git a/test/blocks/sensing_blocks_test.cpp b/test/blocks/sensing_blocks_test.cpp index 267c4ef35..bfa63eee1 100644 --- a/test/blocks/sensing_blocks_test.cpp +++ b/test/blocks/sensing_blocks_test.cpp @@ -367,3 +367,57 @@ TEST_F(SensingBlocksTest, TouchingObject_Invalid_Runtime) ASSERT_FALSE(value_toBool(&value)); value_free(&value); } + +TEST_F(SensingBlocksTest, TouchingColor_String) +{ + auto targetMock = std::make_shared(); + targetMock->setEngine(&m_engineMock); + + ScriptBuilder builder(m_extension.get(), m_engine, targetMock); + builder.addBlock("sensing_touchingcolor"); + builder.addValueInput("COLOR", "#00ffff"); + Block *block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, targetMock.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(targetMock.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(targetMock.get(), &m_engineMock, &script); + + EXPECT_CALL(*targetMock, touchingColor(rgb(0, 255, 255))).WillOnce(Return(true)); + ValueData value = thread.runReporter(); + ASSERT_TRUE(value_toBool(&value)); + value_free(&value); + + EXPECT_CALL(*targetMock, touchingColor(rgb(0, 255, 255))).WillOnce(Return(false)); + value = thread.runReporter(); + ASSERT_FALSE(value_toBool(&value)); + value_free(&value); +} + +TEST_F(SensingBlocksTest, TouchingColor_Number) +{ + auto targetMock = std::make_shared(); + targetMock->setEngine(&m_engineMock); + + ScriptBuilder builder(m_extension.get(), m_engine, targetMock); + builder.addBlock("sensing_touchingcolor"); + builder.addValueInput("COLOR", rgb(255, 54, 23)); + Block *block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, targetMock.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(targetMock.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(targetMock.get(), &m_engineMock, &script); + + EXPECT_CALL(*targetMock, touchingColor(rgb(255, 54, 23))).WillOnce(Return(true)); + ValueData value = thread.runReporter(); + ASSERT_TRUE(value_toBool(&value)); + value_free(&value); + + EXPECT_CALL(*targetMock, touchingColor(rgb(255, 54, 23))).WillOnce(Return(false)); + value = thread.runReporter(); + ASSERT_FALSE(value_toBool(&value)); + value_free(&value); +} From 0d6fd71b3448d8396b828e048bdd9f5b15185f0a Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sat, 9 Aug 2025 09:52:52 +0200 Subject: [PATCH 03/20] Implement sensing_coloristouchingcolor block --- src/blocks/sensingblocks.cpp | 13 +++++++ src/blocks/sensingblocks.h | 1 + test/blocks/sensing_blocks_test.cpp | 56 +++++++++++++++++++++++++++++ 3 files changed, 70 insertions(+) diff --git a/src/blocks/sensingblocks.cpp b/src/blocks/sensingblocks.cpp index 29d2c8b97..323702332 100644 --- a/src/blocks/sensingblocks.cpp +++ b/src/blocks/sensingblocks.cpp @@ -33,6 +33,7 @@ void SensingBlocks::registerBlocks(IEngine *engine) { engine->addCompileFunction(this, "sensing_touchingobject", &compileTouchingObject); engine->addCompileFunction(this, "sensing_touchingcolor", &compileTouchingColor); + engine->addCompileFunction(this, "sensing_coloristouchingcolor", &compileColorIsTouchingColor); } CompilerValue *SensingBlocks::compileTouchingObject(Compiler *compiler) @@ -69,6 +70,13 @@ CompilerValue *SensingBlocks::compileTouchingColor(Compiler *compiler) return compiler->addTargetFunctionCall("sensing_touchingcolor", Compiler::StaticType::Bool, { Compiler::StaticType::Unknown }, { color }); } +CompilerValue *SensingBlocks::compileColorIsTouchingColor(Compiler *compiler) +{ + CompilerValue *color = compiler->addInput("COLOR"); + CompilerValue *color2 = compiler->addInput("COLOR2"); + return compiler->addTargetFunctionCall("sensing_coloristouchingcolor", Compiler::StaticType::Bool, { Compiler::StaticType::Unknown, Compiler::StaticType::Unknown }, { color, color2 }); +} + extern "C" bool sensing_touching_mouse(Target *target) { IEngine *engine = target->engine(); @@ -112,3 +120,8 @@ extern "C" bool sensing_touchingcolor(Target *target, const ValueData *color) { return target->touchingColor(value_toRgba(color)); } + +extern "C" bool sensing_coloristouchingcolor(Target *target, const ValueData *color, const ValueData *color2) +{ + return target->touchingColor(value_toRgba(color), value_toRgba(color2)); +} diff --git a/src/blocks/sensingblocks.h b/src/blocks/sensingblocks.h index a6fd2f4bc..9ca74886b 100644 --- a/src/blocks/sensingblocks.h +++ b/src/blocks/sensingblocks.h @@ -19,6 +19,7 @@ class SensingBlocks : public IExtension private: static CompilerValue *compileTouchingObject(Compiler *compiler); static CompilerValue *compileTouchingColor(Compiler *compiler); + static CompilerValue *compileColorIsTouchingColor(Compiler *compiler); }; } // namespace libscratchcpp diff --git a/test/blocks/sensing_blocks_test.cpp b/test/blocks/sensing_blocks_test.cpp index bfa63eee1..7464fbedf 100644 --- a/test/blocks/sensing_blocks_test.cpp +++ b/test/blocks/sensing_blocks_test.cpp @@ -421,3 +421,59 @@ TEST_F(SensingBlocksTest, TouchingColor_Number) ASSERT_FALSE(value_toBool(&value)); value_free(&value); } + +TEST_F(SensingBlocksTest, ColorIsTouchingColor_StringNumber) +{ + auto targetMock = std::make_shared(); + targetMock->setEngine(&m_engineMock); + + ScriptBuilder builder(m_extension.get(), m_engine, targetMock); + builder.addBlock("sensing_coloristouchingcolor"); + builder.addValueInput("COLOR", "#00ffff"); + builder.addValueInput("COLOR2", rgb(255, 54, 23)); + Block *block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, targetMock.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(targetMock.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(targetMock.get(), &m_engineMock, &script); + + EXPECT_CALL(*targetMock, touchingColor(rgb(0, 255, 255), rgb(255, 54, 23))).WillOnce(Return(true)); + ValueData value = thread.runReporter(); + ASSERT_TRUE(value_toBool(&value)); + value_free(&value); + + EXPECT_CALL(*targetMock, touchingColor(rgb(0, 255, 255), rgb(255, 54, 23))).WillOnce(Return(false)); + value = thread.runReporter(); + ASSERT_FALSE(value_toBool(&value)); + value_free(&value); +} + +TEST_F(SensingBlocksTest, ColorIsTouchingColor_NumberString) +{ + auto targetMock = std::make_shared(); + targetMock->setEngine(&m_engineMock); + + ScriptBuilder builder(m_extension.get(), m_engine, targetMock); + builder.addBlock("sensing_coloristouchingcolor"); + builder.addValueInput("COLOR", rgb(255, 54, 23)); + builder.addValueInput("COLOR2", "#00ffff"); + Block *block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, targetMock.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(targetMock.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(targetMock.get(), &m_engineMock, &script); + + EXPECT_CALL(*targetMock, touchingColor(rgb(255, 54, 23), rgb(0, 255, 255))).WillOnce(Return(true)); + ValueData value = thread.runReporter(); + ASSERT_TRUE(value_toBool(&value)); + value_free(&value); + + EXPECT_CALL(*targetMock, touchingColor(rgb(255, 54, 23), rgb(0, 255, 255))).WillOnce(Return(false)); + value = thread.runReporter(); + ASSERT_FALSE(value_toBool(&value)); + value_free(&value); +} From a587483e0eb8ea807e544620502ef93fbf9486dc Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sat, 9 Aug 2025 11:38:41 +0200 Subject: [PATCH 04/20] Implement sensing_distanceto block --- src/blocks/sensingblocks.cpp | 66 +++++++ src/blocks/sensingblocks.h | 1 + test/blocks/sensing_blocks_test.cpp | 265 ++++++++++++++++++++++++++++ 3 files changed, 332 insertions(+) diff --git a/src/blocks/sensingblocks.cpp b/src/blocks/sensingblocks.cpp index 323702332..eefa70de0 100644 --- a/src/blocks/sensingblocks.cpp +++ b/src/blocks/sensingblocks.cpp @@ -34,6 +34,7 @@ void SensingBlocks::registerBlocks(IEngine *engine) engine->addCompileFunction(this, "sensing_touchingobject", &compileTouchingObject); engine->addCompileFunction(this, "sensing_touchingcolor", &compileTouchingColor); engine->addCompileFunction(this, "sensing_coloristouchingcolor", &compileColorIsTouchingColor); + engine->addCompileFunction(this, "sensing_distanceto", &compileDistanceTo); } CompilerValue *SensingBlocks::compileTouchingObject(Compiler *compiler) @@ -77,6 +78,35 @@ CompilerValue *SensingBlocks::compileColorIsTouchingColor(Compiler *compiler) return compiler->addTargetFunctionCall("sensing_coloristouchingcolor", Compiler::StaticType::Bool, { Compiler::StaticType::Unknown, Compiler::StaticType::Unknown }, { color, color2 }); } +CompilerValue *SensingBlocks::compileDistanceTo(Compiler *compiler) +{ + if (compiler->target()->isStage()) + return compiler->addConstValue(10000.0); + + IEngine *engine = compiler->engine(); + Input *input = compiler->input("DISTANCETOMENU"); + + if (input->pointsToDropdownMenu()) { + std::string value = input->selectedMenuItem(); + + if (value == "_mouse_") + return compiler->addTargetFunctionCall("sensing_distance_to_mouse", Compiler::StaticType::Number); + else if (value != "_stage_") { + Target *target = engine->targetAt(engine->findTarget(value)); + + if (target) { + CompilerValue *targetPtr = compiler->addConstValue(target); + return compiler->addTargetFunctionCall("sensing_distance_to_sprite", Compiler::StaticType::Number, { Compiler::StaticType::Pointer }, { targetPtr }); + } + } + } else { + CompilerValue *object = compiler->addInput(input); + return compiler->addTargetFunctionCall("sensing_distanceto", Compiler::StaticType::Number, { Compiler::StaticType::String }, { object }); + } + + return compiler->addConstValue(10000.0); +} + extern "C" bool sensing_touching_mouse(Target *target) { IEngine *engine = target->engine(); @@ -125,3 +155,39 @@ extern "C" bool sensing_coloristouchingcolor(Target *target, const ValueData *co { return target->touchingColor(value_toRgba(color), value_toRgba(color2)); } + +static inline double sensing_distance(double x0, double y0, double x1, double y1) +{ + return std::sqrt((x1 - x0) * (x1 - x0) + (y1 - y0) * (y1 - y0)); +} + +extern "C" double sensing_distance_to_mouse(Sprite *sprite) +{ + IEngine *engine = sprite->engine(); + return sensing_distance(sprite->x(), sprite->y(), engine->mouseX(), engine->mouseY()); +} + +extern "C" double sensing_distance_to_sprite(Sprite *sprite, Sprite *targetSprite) +{ + return sensing_distance(sprite->x(), sprite->y(), targetSprite->x(), targetSprite->y()); +} + +extern "C" double sensing_distanceto(Sprite *sprite, const StringPtr *object) +{ + static const StringPtr MOUSE_STR("_mouse_"); + static const StringPtr STAGE_STR("_stage_"); + + if (string_compare_case_sensitive(object, &MOUSE_STR) == 0) + return sensing_distance_to_mouse(sprite); + else if (string_compare_case_sensitive(object, &STAGE_STR) != 0) { + IEngine *engine = sprite->engine(); + // TODO: Use UTF-16 in engine + std::string u8name = utf8::utf16to8(std::u16string(object->data)); + Target *objTarget = engine->targetAt(engine->findTarget(u8name)); + + if (objTarget) + return sensing_distance_to_sprite(sprite, static_cast(objTarget)); + } + + return 10000.0; +} diff --git a/src/blocks/sensingblocks.h b/src/blocks/sensingblocks.h index 9ca74886b..98357dcb2 100644 --- a/src/blocks/sensingblocks.h +++ b/src/blocks/sensingblocks.h @@ -20,6 +20,7 @@ class SensingBlocks : public IExtension static CompilerValue *compileTouchingObject(Compiler *compiler); static CompilerValue *compileTouchingColor(Compiler *compiler); static CompilerValue *compileColorIsTouchingColor(Compiler *compiler); + static CompilerValue *compileDistanceTo(Compiler *compiler); }; } // namespace libscratchcpp diff --git a/test/blocks/sensing_blocks_test.cpp b/test/blocks/sensing_blocks_test.cpp index 7464fbedf..d42db0290 100644 --- a/test/blocks/sensing_blocks_test.cpp +++ b/test/blocks/sensing_blocks_test.cpp @@ -477,3 +477,268 @@ TEST_F(SensingBlocksTest, ColorIsTouchingColor_NumberString) ASSERT_FALSE(value_toBool(&value)); value_free(&value); } + +TEST_F(SensingBlocksTest, DistanceTo_Sprite_CompileTime) +{ + auto sprite = std::make_shared(); + sprite->setEngine(&m_engineMock); + + Sprite targetSprite; + + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + builder.addBlock("sensing_distanceto"); + builder.addDropdownInput("DISTANCETOMENU", "Sprite2"); + Block *block = builder.currentBlock(); + + EXPECT_CALL(m_engineMock, findTarget("Sprite2")).WillOnce(Return(3)); + EXPECT_CALL(m_engineMock, targetAt(3)).WillOnce(Return(&targetSprite)); + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(sprite.get(), &m_engineMock, &script); + + sprite->setX(-50.35); + sprite->setY(33.04); + + targetSprite.setX(108.564); + targetSprite.setY(-168.452); + + ValueData value = thread.runReporter(); + ASSERT_EQ(std::round(value_toDouble(&value) * 10000) / 10000, 256.6178); + value_free(&value); +} + +TEST_F(SensingBlocksTest, DistanceTo_Sprite_Runtime) +{ + auto sprite = std::make_shared(); + sprite->setEngine(&m_engineMock); + + Sprite targetSprite; + + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("test_const_string"); + builder.addValueInput("STRING", "Sprite2"); + auto valueBlock = builder.takeBlock(); + + builder.addBlock("sensing_distanceto"); + builder.addObscuredInput("DISTANCETOMENU", valueBlock); + Block *block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(sprite.get(), &m_engineMock, &script); + + sprite->setX(-50.35); + sprite->setY(33.04); + + targetSprite.setX(108.564); + targetSprite.setY(-168.452); + + EXPECT_CALL(m_engineMock, findTarget("Sprite2")).WillRepeatedly(Return(5)); + EXPECT_CALL(m_engineMock, targetAt(5)).WillRepeatedly(Return(&targetSprite)); + + ValueData value = thread.runReporter(); + ASSERT_EQ(std::round(value_toDouble(&value) * 10000) / 10000, 256.6178); + value_free(&value); +} + +TEST_F(SensingBlocksTest, DistanceTo_Sprite_FromStage) +{ + auto stage = std::make_shared(); + stage->setEngine(&m_engineMock); + + Sprite targetSprite; + + ScriptBuilder builder(m_extension.get(), m_engine, stage); + builder.addBlock("sensing_distanceto"); + builder.addDropdownInput("DISTANCETOMENU", "Sprite2"); + Block *block = builder.currentBlock(); + + EXPECT_CALL(m_engineMock, findTarget("Sprite2")).WillRepeatedly(Return(3)); + EXPECT_CALL(m_engineMock, targetAt(3)).WillRepeatedly(Return(&targetSprite)); + Compiler compiler(&m_engineMock, stage.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(stage.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(stage.get(), &m_engineMock, &script); + + targetSprite.setX(108.564); + targetSprite.setY(-168.452); + + ValueData value = thread.runReporter(); + ASSERT_EQ(value_toDouble(&value), 10000.0); + value_free(&value); +} + +TEST_F(SensingBlocksTest, DistanceTo_Stage_CompileTime) +{ + auto sprite = std::make_shared(); + sprite->setEngine(&m_engineMock); + + Stage stage; + + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + builder.addBlock("sensing_distanceto"); + builder.addDropdownInput("DISTANCETOMENU", "_stage_"); + Block *block = builder.currentBlock(); + + EXPECT_CALL(m_engineMock, findTarget("_stage_")).WillRepeatedly(Return(3)); + EXPECT_CALL(m_engineMock, targetAt(3)).WillRepeatedly(Return(&stage)); + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(sprite.get(), &m_engineMock, &script); + + ValueData value = thread.runReporter(); + ASSERT_EQ(value_toDouble(&value), 10000.0); + value_free(&value); +} + +TEST_F(SensingBlocksTest, DistanceTo_Stage_Runtime) +{ + auto sprite = std::make_shared(); + sprite->setEngine(&m_engineMock); + + Stage stage; + + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("test_const_string"); + builder.addValueInput("STRING", "_stage_"); + auto valueBlock = builder.takeBlock(); + + builder.addBlock("sensing_distanceto"); + builder.addObscuredInput("DISTANCETOMENU", valueBlock); + Block *block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(sprite.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, findTarget("_stage_")).WillRepeatedly(Return(5)); + EXPECT_CALL(m_engineMock, targetAt(5)).WillRepeatedly(Return(&stage)); + + ValueData value = thread.runReporter(); + ASSERT_EQ(value_toDouble(&value), 10000.0); + value_free(&value); +} + +TEST_F(SensingBlocksTest, DistanceTo_Mouse_CompileTime) +{ + auto sprite = std::make_shared(); + sprite->setEngine(&m_engineMock); + + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + builder.addBlock("sensing_distanceto"); + builder.addDropdownInput("DISTANCETOMENU", "_mouse_"); + Block *block = builder.currentBlock(); + + EXPECT_CALL(m_engineMock, findTarget).Times(0); + EXPECT_CALL(m_engineMock, targetAt).Times(0); + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(sprite.get(), &m_engineMock, &script); + + sprite->setX(-50.35); + sprite->setY(33.04); + + EXPECT_CALL(m_engineMock, mouseX).WillOnce(Return(-239.98)); + EXPECT_CALL(m_engineMock, mouseY).WillOnce(Return(-86.188)); + ValueData value = thread.runReporter(); + ASSERT_EQ(std::round(value_toDouble(&value) * 10000) / 10000, 223.9974); + value_free(&value); +} + +TEST_F(SensingBlocksTest, DistanceTo_Mouse_Runtime) +{ + auto sprite = std::make_shared(); + sprite->setEngine(&m_engineMock); + + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("test_const_string"); + builder.addValueInput("STRING", "_mouse_"); + auto valueBlock = builder.takeBlock(); + + builder.addBlock("sensing_distanceto"); + builder.addObscuredInput("DISTANCETOMENU", valueBlock); + Block *block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(sprite.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, findTarget).Times(0); + EXPECT_CALL(m_engineMock, targetAt).Times(0); + + sprite->setX(-50.35); + sprite->setY(33.04); + + EXPECT_CALL(m_engineMock, mouseX).WillOnce(Return(-239.98)); + EXPECT_CALL(m_engineMock, mouseY).WillOnce(Return(-86.188)); + ValueData value = thread.runReporter(); + ASSERT_EQ(std::round(value_toDouble(&value) * 10000) / 10000, 223.9974); + value_free(&value); +} + +TEST_F(SensingBlocksTest, DistanceTo_Invalid_CompileTime) +{ + auto sprite = std::make_shared(); + sprite->setEngine(&m_engineMock); + + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + builder.addBlock("sensing_distanceto"); + builder.addDropdownInput("DISTANCETOMENU", "test"); + Block *block = builder.currentBlock(); + + EXPECT_CALL(m_engineMock, findTarget("test")).WillOnce(Return(-1)); + EXPECT_CALL(m_engineMock, targetAt).WillRepeatedly(Return(nullptr)); + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(sprite.get(), &m_engineMock, &script); + + ValueData value = thread.runReporter(); + ASSERT_EQ(value_toDouble(&value), 10000.0); + value_free(&value); +} + +TEST_F(SensingBlocksTest, DistanceTo_Invalid_Runtime) +{ + auto sprite = std::make_shared(); + sprite->setEngine(&m_engineMock); + + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("test_const_string"); + builder.addValueInput("STRING", "test"); + auto valueBlock = builder.takeBlock(); + + builder.addBlock("sensing_distanceto"); + builder.addObscuredInput("DISTANCETOMENU", valueBlock); + Block *block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(sprite.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, findTarget("test")).WillRepeatedly(Return(-1)); + EXPECT_CALL(m_engineMock, targetAt).WillRepeatedly(Return(nullptr)); + ValueData value = thread.runReporter(); + ASSERT_EQ(value_toDouble(&value), 10000.0); + value_free(&value); +} From 6f9ebdf5dbf326acb133e011068cc21ebbb83646 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sat, 9 Aug 2025 11:47:37 +0200 Subject: [PATCH 05/20] Remove obsolete stage check from touching object block --- src/blocks/sensingblocks.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/blocks/sensingblocks.cpp b/src/blocks/sensingblocks.cpp index eefa70de0..3f1bdff48 100644 --- a/src/blocks/sensingblocks.cpp +++ b/src/blocks/sensingblocks.cpp @@ -52,7 +52,7 @@ CompilerValue *SensingBlocks::compileTouchingObject(Compiler *compiler) else if (value != "_stage_") { Target *target = engine->targetAt(engine->findTarget(value)); - if (target && !target->isStage()) { + if (target) { CompilerValue *targetPtr = compiler->addConstValue(target); return compiler->addTargetFunctionCall("sensing_touching_sprite", Compiler::StaticType::Bool, { Compiler::StaticType::Pointer }, { targetPtr }); } From 086ca12a4bb6b7bd766e0a10805706b7b783b1f3 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sat, 9 Aug 2025 17:41:55 +0200 Subject: [PATCH 06/20] Add answer getter to engine --- include/scratchcpp/iengine.h | 7 +++++++ src/engine/internal/engine.cpp | 13 +++++++++++++ src/engine/internal/engine.h | 3 +++ test/engine/engine_test.cpp | 13 ++++++++++++- test/mocks/enginemock.h | 2 ++ 5 files changed, 37 insertions(+), 1 deletion(-) diff --git a/include/scratchcpp/iengine.h b/include/scratchcpp/iengine.h index bf21ddf4a..821615f44 100644 --- a/include/scratchcpp/iengine.h +++ b/include/scratchcpp/iengine.h @@ -24,6 +24,7 @@ class Stage; class Variable; class List; class Script; +class StringPtr; class Thread; class ITimer; class KeyEvent; @@ -182,6 +183,12 @@ class LIBSCRATCHCPP_EXPORT IEngine /*! Call this when a target is clicked. */ virtual void clickTarget(Target *target) = 0; + /*! + * Returns the answer received from the user. + * \see questionAnswered() + */ + virtual const StringPtr *answer() const = 0; + /*! Returns the stage width. */ virtual unsigned int stageWidth() const = 0; diff --git a/src/engine/internal/engine.cpp b/src/engine/internal/engine.cpp index 176e6b663..72dbbb3b8 100644 --- a/src/engine/internal/engine.cpp +++ b/src/engine/internal/engine.cpp @@ -50,12 +50,20 @@ Engine::Engine() : m_clock(Clock::instance().get()), m_audioEngine(IAudioEngine::instance()) { + m_answer = string_pool_new(); + string_assign_cstring(m_answer, ""); + + m_questionAnswered.connect([this](const std::string &answer) { + // Update answer + string_assign_cstring(m_answer, answer.c_str()); + }); } Engine::~Engine() { m_clones.clear(); m_sortedDrawables.clear(); + string_pool_free(m_answer); } void Engine::clear() @@ -848,6 +856,11 @@ void Engine::clickTarget(Target *target) } } +const StringPtr *Engine::answer() const +{ + return m_answer; +} + unsigned int Engine::stageWidth() const { return m_stageWidth; diff --git a/src/engine/internal/engine.h b/src/engine/internal/engine.h index e2e1018b2..19e9809d5 100644 --- a/src/engine/internal/engine.h +++ b/src/engine/internal/engine.h @@ -84,6 +84,8 @@ class Engine : public IEngine void clickTarget(Target *target) override; + const StringPtr *answer() const override; + unsigned int stageWidth() const override; void setStageWidth(unsigned int width) override; @@ -273,6 +275,7 @@ class Engine : public IEngine double m_mouseX = 0; double m_mouseY = 0; bool m_mousePressed = false; + StringPtr *m_answer = nullptr; unsigned int m_stageWidth = 480; unsigned int m_stageHeight = 360; int m_cloneLimit = 300; diff --git a/test/engine/engine_test.cpp b/test/engine/engine_test.cpp index 9b97a1910..174ac50a3 100644 --- a/test/engine/engine_test.cpp +++ b/test/engine/engine_test.cpp @@ -1694,8 +1694,19 @@ TEST(EngineTest, CreateMissingMonitors) } } -void questionFunction(const std::string &) +TEST(EngineTest, Answer) { + Engine engine; + const StringPtr empty(""); + const StringPtr test("test"); + const StringPtr *answer = engine.answer(); + + ASSERT_TRUE(answer); + ASSERT_EQ(string_compare_case_sensitive(answer, &empty), 0); + + engine.questionAnswered()("test"); + answer = engine.answer(); + ASSERT_EQ(string_compare_case_sensitive(answer, &test), 0); } TEST(EngineTest, Clones) diff --git a/test/mocks/enginemock.h b/test/mocks/enginemock.h index a57be78df..f7378f515 100644 --- a/test/mocks/enginemock.h +++ b/test/mocks/enginemock.h @@ -66,6 +66,8 @@ class EngineMock : public IEngine MOCK_METHOD(void, clickTarget, (Target * target), (override)); + MOCK_METHOD(const StringPtr *, answer, (), (const, override)); + MOCK_METHOD(unsigned int, stageWidth, (), (const, override)); MOCK_METHOD(void, setStageWidth, (unsigned int), (override)); From 8439b3a2acaa1e116e8911734729e2bb69241a3b Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sat, 9 Aug 2025 17:43:09 +0200 Subject: [PATCH 07/20] Implement sensing_askandwait and sensing_answer blocks --- src/blocks/sensingblocks.cpp | 123 ++++++++ src/blocks/sensingblocks.h | 33 +++ test/blocks/sensing_blocks_test.cpp | 433 ++++++++++++++++++++++++++++ 3 files changed, 589 insertions(+) diff --git a/src/blocks/sensingblocks.cpp b/src/blocks/sensingblocks.cpp index 3f1bdff48..6a7d165e6 100644 --- a/src/blocks/sensingblocks.cpp +++ b/src/blocks/sensingblocks.cpp @@ -3,11 +3,16 @@ #include #include #include +#include +#include +#include #include #include #include +#include #include #include +#include #include #include "sensingblocks.h" @@ -35,6 +40,41 @@ void SensingBlocks::registerBlocks(IEngine *engine) engine->addCompileFunction(this, "sensing_touchingcolor", &compileTouchingColor); engine->addCompileFunction(this, "sensing_coloristouchingcolor", &compileColorIsTouchingColor); engine->addCompileFunction(this, "sensing_distanceto", &compileDistanceTo); + engine->addCompileFunction(this, "sensing_askandwait", &compileAskAndWait); + engine->addCompileFunction(this, "sensing_answer", &compileAnswer); +} + +void SensingBlocks::onInit(IEngine *engine) +{ + engine->questionAnswered().connect(&onAnswer); + + engine->threadAboutToStop().connect([engine](Thread *thread) { + if (!m_questions.empty()) { + // Abort the question of this thread if it's currently being displayed + if (m_questions.front()->thread == thread) { + thread->target()->bubble()->setText(""); + engine->questionAborted()(); + } + + m_questions.erase(std::remove_if(m_questions.begin(), m_questions.end(), [thread](const std::unique_ptr &question) { return question->thread == thread; }), m_questions.end()); + } + }); +} + +void SensingBlocks::clearQuestions() +{ + m_questions.clear(); +} + +void SensingBlocks::askQuestion(ExecutionContext *ctx, const StringPtr *question) +{ + const bool isQuestionAsked = !m_questions.empty(); + // TODO: Use UTF-16 in engine (and TextBubble?) + std::string u8str = utf8::utf16to8(std::u16string(question->data)); + enqueueAsk(u8str, ctx->thread()); + + if (!isQuestionAsked) + askNextQuestion(); } CompilerValue *SensingBlocks::compileTouchingObject(Compiler *compiler) @@ -107,6 +147,76 @@ CompilerValue *SensingBlocks::compileDistanceTo(Compiler *compiler) return compiler->addConstValue(10000.0); } +CompilerValue *SensingBlocks::compileAskAndWait(Compiler *compiler) +{ + CompilerValue *question = compiler->addInput("QUESTION"); + compiler->addFunctionCallWithCtx("sensing_askandwait", Compiler::StaticType::Void, { Compiler::StaticType::String }, { question }); + compiler->createYield(); + return nullptr; +} + +CompilerValue *SensingBlocks::compileAnswer(Compiler *compiler) +{ + return compiler->addFunctionCallWithCtx("sensing_answer", Compiler::StaticType::String); +} + +void SensingBlocks::onAnswer(const std::string &answer) +{ + // https://github.com/scratchfoundation/scratch-vm/blob/6055823f203a696165084b873e661713806583ec/src/blocks/scratch3_sensing.js#L99-L115 + if (!m_questions.empty()) { + Question *question = m_questions.front().get(); + Thread *thread = question->thread; + assert(thread); + assert(thread->target()); + + // If the target was visible when asked, hide the say bubble unless the target was the stage + if (question->wasVisible && !question->wasStage) + thread->target()->bubble()->setText(""); + + m_questions.erase(m_questions.begin()); + thread->promise()->resolve(); + askNextQuestion(); + } +} + +void SensingBlocks::enqueueAsk(const std::string &question, Thread *thread) +{ + // https://github.com/scratchfoundation/scratch-vm/blob/6055823f203a696165084b873e661713806583ec/src/blocks/scratch3_sensing.js#L117-L119 + assert(thread); + Target *target = thread->target(); + assert(target); + bool visible = true; + bool isStage = target->isStage(); + + if (!isStage) { + Sprite *sprite = static_cast(target); + visible = sprite->visible(); + } + + m_questions.push_back(std::make_unique(question, thread, visible, isStage)); +} + +void SensingBlocks::askNextQuestion() +{ + // https://github.com/scratchfoundation/scratch-vm/blob/6055823f203a696165084b873e661713806583ec/src/blocks/scratch3_sensing.js#L121-L133 + if (m_questions.empty()) + return; + + Question *question = m_questions.front().get(); + Target *target = question->thread->target(); + IEngine *engine = question->thread->engine(); + + // If the target is visible, emit a blank question and show + // a bubble unless the target was the stage + if (question->wasVisible && !question->wasStage) { + target->bubble()->setType(TextBubble::Type::Say); + target->bubble()->setText(question->question); + + engine->questionAsked()(""); + } else + engine->questionAsked()(question->question); +} + extern "C" bool sensing_touching_mouse(Target *target) { IEngine *engine = target->engine(); @@ -191,3 +301,16 @@ extern "C" double sensing_distanceto(Sprite *sprite, const StringPtr *object) return 10000.0; } + +extern "C" void sensing_askandwait(ExecutionContext *ctx, const StringPtr *question) +{ + SensingBlocks::askQuestion(ctx, question); + ctx->thread()->setPromise(std::make_shared()); +} + +extern "C" StringPtr *sensing_answer(ExecutionContext *ctx) +{ + StringPtr *ret = string_pool_new(); + string_assign(ret, ctx->engine()->answer()); + return ret; +} diff --git a/src/blocks/sensingblocks.h b/src/blocks/sensingblocks.h index 98357dcb2..6b13d6577 100644 --- a/src/blocks/sensingblocks.h +++ b/src/blocks/sensingblocks.h @@ -3,10 +3,15 @@ #pragma once #include +#include +#include namespace libscratchcpp { +class ExecutionContext; +class Thread; + class SensingBlocks : public IExtension { public: @@ -15,12 +20,40 @@ class SensingBlocks : public IExtension Rgb color() const override; void registerBlocks(IEngine *engine) override; + void onInit(IEngine *engine) override; + + static void clearQuestions(); + static void askQuestion(ExecutionContext *ctx, const StringPtr *question); private: + struct Question + { + Question(const std::string &question, Thread *thread, bool wasVisible, bool wasStage) : + question(question), + thread(thread), + wasVisible(wasVisible), + wasStage(wasStage) + { + } + + std::string question; + Thread *thread = nullptr; + bool wasVisible = false; + bool wasStage = false; + }; + static CompilerValue *compileTouchingObject(Compiler *compiler); static CompilerValue *compileTouchingColor(Compiler *compiler); static CompilerValue *compileColorIsTouchingColor(Compiler *compiler); static CompilerValue *compileDistanceTo(Compiler *compiler); + static CompilerValue *compileAskAndWait(Compiler *compiler); + static CompilerValue *compileAnswer(Compiler *compiler); + + static void onAnswer(const std::string &answer); + static void enqueueAsk(const std::string &question, Thread *thread); + static void askNextQuestion(); + + static inline std::vector> m_questions; }; } // namespace libscratchcpp diff --git a/test/blocks/sensing_blocks_test.cpp b/test/blocks/sensing_blocks_test.cpp index d42db0290..4798d4227 100644 --- a/test/blocks/sensing_blocks_test.cpp +++ b/test/blocks/sensing_blocks_test.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -17,18 +18,28 @@ using namespace libscratchcpp; using namespace libscratchcpp::test; using ::testing::Return; +using ::testing::ReturnRef; class SensingBlocksTest : public testing::Test { public: + struct QuestionSpy + { + MOCK_METHOD(void, asked, (const std::string &), ()); + MOCK_METHOD(void, aborted, (), ()); + }; + void SetUp() override { m_extension = std::make_unique(); m_engine = m_project.engine().get(); m_extension->registerBlocks(m_engine); + m_extension->onInit(m_engine); registerBlocks(m_engine, m_extension.get()); } + void TearDown() override { SensingBlocks::clearQuestions(); } + std::unique_ptr m_extension; Project m_project; IEngine *m_engine = nullptr; @@ -742,3 +753,425 @@ TEST_F(SensingBlocksTest, DistanceTo_Invalid_Runtime) ASSERT_EQ(value_toDouble(&value), 10000.0); value_free(&value); } + +TEST_F(SensingBlocksTest, AskAndWait_VisibleSprite) +{ + auto sprite = std::make_shared(); + sprite->setEngine(&m_engineMock); + + // Build + ScriptBuilder builder1(m_extension.get(), m_engine, sprite); + builder1.addBlock("sensing_askandwait"); + builder1.addValueInput("QUESTION", "test"); + Block *block1 = builder1.currentBlock(); + + Compiler compiler1(&m_engineMock, sprite.get()); + auto code1 = compiler1.compile(block1); + Script script1(sprite.get(), block1, &m_engineMock); + script1.setCode(code1); + Thread thread1(sprite.get(), &m_engineMock, &script1); + + ScriptBuilder builder2(m_extension.get(), m_engine, sprite); + builder2.addBlock("sensing_answer"); + Block *block2 = builder2.currentBlock(); + + Compiler compiler2(m_engine, sprite.get()); + auto code2 = compiler2.compile(block2, Compiler::CodeType::Reporter); + Script script2(sprite.get(), block2, m_engine); + script2.setCode(code2); + Thread thread2(sprite.get(), m_engine, &script2); + + // Run + QuestionSpy spy; + auto asked = std::bind(&QuestionSpy::asked, &spy, std::placeholders::_1); + sigslot::signal askedSignal; + askedSignal.connect(asked); + + sprite->bubble()->setType(TextBubble::Type::Think); + sprite->setVisible(true); + + EXPECT_CALL(m_engineMock, questionAsked()).WillOnce(ReturnRef(askedSignal)); + EXPECT_CALL(spy, asked("")); // visible => empty question + thread1.run(); + ASSERT_EQ(sprite->bubble()->type(), TextBubble::Type::Say); + ASSERT_EQ(sprite->bubble()->text(), "test"); + + ValueData value = thread2.runReporter(); + std::string str; + value_toString(&value, &str); + value_free(&value); + ASSERT_EQ(str, ""); // not answered yet + + ASSERT_FALSE(thread1.isFinished()); + + // Answer + m_engine->questionAnswered()("test answer"); + ASSERT_TRUE(sprite->bubble()->text().empty()); + + value = thread2.runReporter(); + value_toString(&value, &str); + value_free(&value); + ASSERT_EQ(str, "test answer"); + + thread1.run(); + ASSERT_TRUE(thread1.isFinished()); +} + +TEST_F(SensingBlocksTest, AskAndWait_VisibleSpriteBefore) +{ + auto sprite = std::make_shared(); + sprite->setEngine(&m_engineMock); + + // Build + ScriptBuilder builder1(m_extension.get(), m_engine, sprite); + builder1.addBlock("sensing_askandwait"); + builder1.addValueInput("QUESTION", "test"); + Block *block1 = builder1.currentBlock(); + + Compiler compiler1(&m_engineMock, sprite.get()); + auto code1 = compiler1.compile(block1); + Script script1(sprite.get(), block1, &m_engineMock); + script1.setCode(code1); + Thread thread1(sprite.get(), &m_engineMock, &script1); + + ScriptBuilder builder2(m_extension.get(), m_engine, sprite); + builder2.addBlock("sensing_answer"); + Block *block2 = builder2.currentBlock(); + + Compiler compiler2(m_engine, sprite.get()); + auto code2 = compiler2.compile(block2, Compiler::CodeType::Reporter); + Script script2(sprite.get(), block2, m_engine); + script2.setCode(code2); + Thread thread2(sprite.get(), m_engine, &script2); + + // Run + QuestionSpy spy; + auto asked = std::bind(&QuestionSpy::asked, &spy, std::placeholders::_1); + sigslot::signal askedSignal; + askedSignal.connect(asked); + + sprite->bubble()->setType(TextBubble::Type::Think); + sprite->setVisible(true); + + EXPECT_CALL(m_engineMock, questionAsked()).WillOnce(ReturnRef(askedSignal)); + EXPECT_CALL(spy, asked("")); // visible => empty question + thread1.run(); + ASSERT_EQ(sprite->bubble()->type(), TextBubble::Type::Say); + ASSERT_EQ(sprite->bubble()->text(), "test"); + + ASSERT_FALSE(thread1.isFinished()); + + // Answer + sprite->setVisible(false); + m_engine->questionAnswered()("test answer"); + ASSERT_TRUE(sprite->bubble()->text().empty()); // sprite was visible when asked + + ValueData value = thread2.runReporter(); + std::string str; + value_toString(&value, &str); + value_free(&value); + ASSERT_EQ(str, "test answer"); + + thread1.run(); + ASSERT_TRUE(thread1.isFinished()); +} + +TEST_F(SensingBlocksTest, AskAndWait_InvisibleSprite) +{ + auto sprite = std::make_shared(); + sprite->setEngine(&m_engineMock); + + // Build + ScriptBuilder builder1(m_extension.get(), m_engine, sprite); + builder1.addBlock("sensing_askandwait"); + builder1.addValueInput("QUESTION", "test"); + Block *block1 = builder1.currentBlock(); + + Compiler compiler1(&m_engineMock, sprite.get()); + auto code1 = compiler1.compile(block1); + Script script1(sprite.get(), block1, &m_engineMock); + script1.setCode(code1); + Thread thread1(sprite.get(), &m_engineMock, &script1); + + ScriptBuilder builder2(m_extension.get(), m_engine, sprite); + builder2.addBlock("sensing_answer"); + Block *block2 = builder2.currentBlock(); + + Compiler compiler2(m_engine, sprite.get()); + auto code2 = compiler2.compile(block2, Compiler::CodeType::Reporter); + Script script2(sprite.get(), block2, m_engine); + script2.setCode(code2); + Thread thread2(sprite.get(), m_engine, &script2); + + // Run + QuestionSpy spy; + auto asked = std::bind(&QuestionSpy::asked, &spy, std::placeholders::_1); + sigslot::signal askedSignal; + askedSignal.connect(asked); + + sprite->bubble()->setType(TextBubble::Type::Think); + sprite->bubble()->setText("hello"); + sprite->setVisible(false); + + EXPECT_CALL(m_engineMock, questionAsked()).WillOnce(ReturnRef(askedSignal)); + EXPECT_CALL(spy, asked("test")); + thread1.run(); + ASSERT_EQ(sprite->bubble()->type(), TextBubble::Type::Think); + ASSERT_EQ(sprite->bubble()->text(), "hello"); + + ASSERT_FALSE(thread1.isFinished()); + + // Answer + m_engine->questionAnswered()("test answer"); + ASSERT_EQ(sprite->bubble()->type(), TextBubble::Type::Think); + ASSERT_EQ(sprite->bubble()->text(), "hello"); + + ValueData value = thread2.runReporter(); + std::string str; + value_toString(&value, &str); + value_free(&value); + ASSERT_EQ(str, "test answer"); + + thread1.run(); + ASSERT_TRUE(thread1.isFinished()); +} + +TEST_F(SensingBlocksTest, AskAndWait_InvisibleSpriteBefore) +{ + auto sprite = std::make_shared(); + sprite->setEngine(&m_engineMock); + + // Build + ScriptBuilder builder1(m_extension.get(), m_engine, sprite); + builder1.addBlock("sensing_askandwait"); + builder1.addValueInput("QUESTION", "test"); + Block *block1 = builder1.currentBlock(); + + Compiler compiler1(&m_engineMock, sprite.get()); + auto code1 = compiler1.compile(block1); + Script script1(sprite.get(), block1, &m_engineMock); + script1.setCode(code1); + Thread thread1(sprite.get(), &m_engineMock, &script1); + + ScriptBuilder builder2(m_extension.get(), m_engine, sprite); + builder2.addBlock("sensing_answer"); + Block *block2 = builder2.currentBlock(); + + Compiler compiler2(m_engine, sprite.get()); + auto code2 = compiler2.compile(block2, Compiler::CodeType::Reporter); + Script script2(sprite.get(), block2, m_engine); + script2.setCode(code2); + Thread thread2(sprite.get(), m_engine, &script2); + + // Run + QuestionSpy spy; + auto asked = std::bind(&QuestionSpy::asked, &spy, std::placeholders::_1); + sigslot::signal askedSignal; + askedSignal.connect(asked); + + sprite->bubble()->setType(TextBubble::Type::Think); + sprite->bubble()->setText("hello"); + sprite->setVisible(false); + + EXPECT_CALL(m_engineMock, questionAsked()).WillOnce(ReturnRef(askedSignal)); + EXPECT_CALL(spy, asked("test")); + thread1.run(); + ASSERT_EQ(sprite->bubble()->type(), TextBubble::Type::Think); + ASSERT_EQ(sprite->bubble()->text(), "hello"); + + ASSERT_FALSE(thread1.isFinished()); + + // Answer + sprite->setVisible(true); + m_engine->questionAnswered()("test answer"); + ASSERT_EQ(sprite->bubble()->type(), TextBubble::Type::Think); + ASSERT_EQ(sprite->bubble()->text(), "hello"); // sprite was invisible when asked + + ValueData value = thread2.runReporter(); + std::string str; + value_toString(&value, &str); + ASSERT_EQ(str, "test answer"); + value_free(&value); + + thread1.run(); + ASSERT_TRUE(thread1.isFinished()); +} + +TEST_F(SensingBlocksTest, AskAndWait_Stage) +{ + auto stage = std::make_shared(); + stage->setEngine(&m_engineMock); + + // Build + ScriptBuilder builder1(m_extension.get(), m_engine, stage); + builder1.addBlock("sensing_askandwait"); + builder1.addValueInput("QUESTION", "test"); + Block *block1 = builder1.currentBlock(); + + Compiler compiler1(&m_engineMock, stage.get()); + auto code1 = compiler1.compile(block1); + Script script1(stage.get(), block1, &m_engineMock); + script1.setCode(code1); + Thread thread1(stage.get(), &m_engineMock, &script1); + + ScriptBuilder builder2(m_extension.get(), m_engine, stage); + builder2.addBlock("sensing_answer"); + Block *block2 = builder2.currentBlock(); + + Compiler compiler2(m_engine, stage.get()); + auto code2 = compiler2.compile(block2, Compiler::CodeType::Reporter); + Script script2(stage.get(), block2, m_engine); + script2.setCode(code2); + Thread thread2(stage.get(), m_engine, &script2); + + ASSERT_FALSE(thread1.isFinished()); + + // Run + QuestionSpy spy; + auto asked = std::bind(&QuestionSpy::asked, &spy, std::placeholders::_1); + sigslot::signal askedSignal; + askedSignal.connect(asked); + + stage->bubble()->setType(TextBubble::Type::Think); + stage->bubble()->setText("hello"); + + EXPECT_CALL(m_engineMock, questionAsked()).WillOnce(ReturnRef(askedSignal)); + EXPECT_CALL(spy, asked("test")); + thread1.run(); + ASSERT_EQ(stage->bubble()->type(), TextBubble::Type::Think); + ASSERT_EQ(stage->bubble()->text(), "hello"); + + // Answer + m_engine->questionAnswered()("test answer"); + ASSERT_EQ(stage->bubble()->type(), TextBubble::Type::Think); + ASSERT_EQ(stage->bubble()->text(), "hello"); + + ValueData value = thread2.runReporter(); + std::string str; + value_toString(&value, &str); + value_free(&value); + ASSERT_EQ(str, "test answer"); + + thread1.run(); + ASSERT_TRUE(thread1.isFinished()); +} + +TEST_F(SensingBlocksTest, AskAndWait_MultipleQuestions) +{ + auto sprite = std::make_shared(); + sprite->setEngine(&m_engineMock); + + // Build + ScriptBuilder builder1(m_extension.get(), m_engine, sprite); + + builder1.addBlock("sensing_askandwait"); + builder1.addValueInput("QUESTION", "test1"); + Block *block1 = builder1.currentBlock(); + + builder1.addBlock("sensing_askandwait"); + builder1.addValueInput("QUESTION", "test2"); + builder1.currentBlock(); // force build + + Compiler compiler1(&m_engineMock, sprite.get()); + auto code1 = compiler1.compile(block1); + Script script1(sprite.get(), block1, &m_engineMock); + script1.setCode(code1); + Thread thread1(sprite.get(), &m_engineMock, &script1); + + ScriptBuilder builder2(m_extension.get(), m_engine, sprite); + builder2.addBlock("sensing_answer"); + Block *block2 = builder2.currentBlock(); + + Compiler compiler2(m_engine, sprite.get()); + auto code2 = compiler2.compile(block2, Compiler::CodeType::Reporter); + Script script2(sprite.get(), block2, m_engine); + script2.setCode(code2); + Thread thread2(sprite.get(), m_engine, &script2); + + // Run + QuestionSpy spy; + auto asked = std::bind(&QuestionSpy::asked, &spy, std::placeholders::_1); + sigslot::signal askedSignal; + askedSignal.connect(asked); + + sprite->setVisible(false); + + // Ask (1/2) + EXPECT_CALL(m_engineMock, questionAsked()).WillOnce(ReturnRef(askedSignal)); + EXPECT_CALL(spy, asked("test1")); + thread1.run(); + ASSERT_FALSE(thread1.isFinished()); + + // Answer (1/2) + m_engine->questionAnswered()("test answer 1"); + + ValueData value = thread2.runReporter(); + std::string str; + value_toString(&value, &str); + value_free(&value); + ASSERT_EQ(str, "test answer 1"); + + // Ask (2/2) + EXPECT_CALL(m_engineMock, questionAsked()).WillOnce(ReturnRef(askedSignal)); + EXPECT_CALL(spy, asked("test2")); + thread1.run(); + ASSERT_FALSE(thread1.isFinished()); + + // Answer (2/2) + m_engine->questionAnswered()("test answer 2"); + + value = thread2.runReporter(); + value_toString(&value, &str); + value_free(&value); + ASSERT_EQ(str, "test answer 2"); + + EXPECT_CALL(m_engineMock, questionAsked).Times(0); + thread1.run(); + ASSERT_TRUE(thread1.isFinished()); +} + +TEST_F(SensingBlocksTest, AskAndWait_KillThread) +{ + auto sprite = std::make_shared(); + sprite->setEngine(&m_engineMock); + + // Build + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + builder.addBlock("sensing_askandwait"); + builder.addValueInput("QUESTION", "test"); + Block *block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread1(sprite.get(), &m_engineMock, &script); + + // Run + QuestionSpy spy; + auto asked = std::bind(&QuestionSpy::asked, &spy, std::placeholders::_1); + sigslot::signal askedSignal; + askedSignal.connect(asked); + + sprite->bubble()->setText("hello"); + sprite->setVisible(false); + + EXPECT_CALL(m_engineMock, questionAsked()).WillOnce(ReturnRef(askedSignal)); + EXPECT_CALL(spy, asked); + thread1.run(); + + // Kill + auto aborted = std::bind(&QuestionSpy::aborted, &spy); + m_engine->questionAborted().connect(aborted); + + EXPECT_CALL(spy, aborted()); + m_engine->threadAboutToStop()(&thread1); + thread1.kill(); + ASSERT_TRUE(sprite->bubble()->text().empty()); + + // Running the script again should work because the question has been removed + EXPECT_CALL(m_engineMock, questionAsked()).WillOnce(ReturnRef(askedSignal)); + EXPECT_CALL(spy, asked); + Thread thread2(sprite.get(), &m_engineMock, &script); + thread2.run(); +} From bf01dd9857cc19c93c4410aed112667889fa9873 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sun, 10 Aug 2025 09:44:32 +0200 Subject: [PATCH 08/20] Implement sensing_keypressed block --- src/blocks/sensingblocks.cpp | 14 ++++++++ src/blocks/sensingblocks.h | 1 + test/blocks/sensing_blocks_test.cpp | 54 +++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+) diff --git a/src/blocks/sensingblocks.cpp b/src/blocks/sensingblocks.cpp index 6a7d165e6..0ab79f36c 100644 --- a/src/blocks/sensingblocks.cpp +++ b/src/blocks/sensingblocks.cpp @@ -42,6 +42,7 @@ void SensingBlocks::registerBlocks(IEngine *engine) engine->addCompileFunction(this, "sensing_distanceto", &compileDistanceTo); engine->addCompileFunction(this, "sensing_askandwait", &compileAskAndWait); engine->addCompileFunction(this, "sensing_answer", &compileAnswer); + engine->addCompileFunction(this, "sensing_keypressed", &compileKeyPressed); } void SensingBlocks::onInit(IEngine *engine) @@ -160,6 +161,12 @@ CompilerValue *SensingBlocks::compileAnswer(Compiler *compiler) return compiler->addFunctionCallWithCtx("sensing_answer", Compiler::StaticType::String); } +CompilerValue *SensingBlocks::compileKeyPressed(Compiler *compiler) +{ + CompilerValue *key = compiler->addInput("KEY_OPTION"); + return compiler->addFunctionCallWithCtx("sensing_keypressed", Compiler::StaticType::Bool, { Compiler::StaticType::String }, { key }); +} + void SensingBlocks::onAnswer(const std::string &answer) { // https://github.com/scratchfoundation/scratch-vm/blob/6055823f203a696165084b873e661713806583ec/src/blocks/scratch3_sensing.js#L99-L115 @@ -314,3 +321,10 @@ extern "C" StringPtr *sensing_answer(ExecutionContext *ctx) string_assign(ret, ctx->engine()->answer()); return ret; } + +extern "C" bool sensing_keypressed(ExecutionContext *ctx, const StringPtr *key) +{ + // TODO: Use UTF-16 in engine + std::string u8name = utf8::utf16to8(std::u16string(key->data)); + return ctx->engine()->keyPressed(u8name); +} diff --git a/src/blocks/sensingblocks.h b/src/blocks/sensingblocks.h index 6b13d6577..1bebf79bc 100644 --- a/src/blocks/sensingblocks.h +++ b/src/blocks/sensingblocks.h @@ -48,6 +48,7 @@ class SensingBlocks : public IExtension static CompilerValue *compileDistanceTo(Compiler *compiler); static CompilerValue *compileAskAndWait(Compiler *compiler); static CompilerValue *compileAnswer(Compiler *compiler); + static CompilerValue *compileKeyPressed(Compiler *compiler); static void onAnswer(const std::string &answer); static void enqueueAsk(const std::string &question, Thread *thread); diff --git a/test/blocks/sensing_blocks_test.cpp b/test/blocks/sensing_blocks_test.cpp index 4798d4227..ad95fb727 100644 --- a/test/blocks/sensing_blocks_test.cpp +++ b/test/blocks/sensing_blocks_test.cpp @@ -1175,3 +1175,57 @@ TEST_F(SensingBlocksTest, AskAndWait_KillThread) Thread thread2(sprite.get(), &m_engineMock, &script); thread2.run(); } + +TEST_F(SensingBlocksTest, KeyPressed_Space) +{ + auto targetMock = std::make_shared(); + targetMock->setEngine(&m_engineMock); + + ScriptBuilder builder(m_extension.get(), m_engine, targetMock); + builder.addBlock("sensing_keypressed"); + builder.addDropdownInput("KEY_OPTION", "space"); + Block *block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, targetMock.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(targetMock.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(targetMock.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, keyPressed("space")).WillOnce(Return(true)); + ValueData value = thread.runReporter(); + ASSERT_TRUE(value_toBool(&value)); + value_free(&value); + + EXPECT_CALL(m_engineMock, keyPressed("space")).WillOnce(Return(false)); + value = thread.runReporter(); + ASSERT_FALSE(value_toBool(&value)); + value_free(&value); +} + +TEST_F(SensingBlocksTest, KeyPressed_M) +{ + auto targetMock = std::make_shared(); + targetMock->setEngine(&m_engineMock); + + ScriptBuilder builder(m_extension.get(), m_engine, targetMock); + builder.addBlock("sensing_keypressed"); + builder.addDropdownInput("KEY_OPTION", "m"); + Block *block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, targetMock.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(targetMock.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(targetMock.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, keyPressed("m")).WillOnce(Return(true)); + ValueData value = thread.runReporter(); + ASSERT_TRUE(value_toBool(&value)); + value_free(&value); + + EXPECT_CALL(m_engineMock, keyPressed("m")).WillOnce(Return(false)); + value = thread.runReporter(); + ASSERT_FALSE(value_toBool(&value)); + value_free(&value); +} From 365c6a9997f0d1f5bec19daacd84c8a3095880eb Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sun, 10 Aug 2025 09:48:30 +0200 Subject: [PATCH 09/20] Implement sensing_mousedown block --- src/blocks/sensingblocks.cpp | 11 +++++++++++ src/blocks/sensingblocks.h | 1 + test/blocks/sensing_blocks_test.cpp | 26 ++++++++++++++++++++++++++ 3 files changed, 38 insertions(+) diff --git a/src/blocks/sensingblocks.cpp b/src/blocks/sensingblocks.cpp index 0ab79f36c..762147e66 100644 --- a/src/blocks/sensingblocks.cpp +++ b/src/blocks/sensingblocks.cpp @@ -43,6 +43,7 @@ void SensingBlocks::registerBlocks(IEngine *engine) engine->addCompileFunction(this, "sensing_askandwait", &compileAskAndWait); engine->addCompileFunction(this, "sensing_answer", &compileAnswer); engine->addCompileFunction(this, "sensing_keypressed", &compileKeyPressed); + engine->addCompileFunction(this, "sensing_mousedown", &compileMouseDown); } void SensingBlocks::onInit(IEngine *engine) @@ -167,6 +168,11 @@ CompilerValue *SensingBlocks::compileKeyPressed(Compiler *compiler) return compiler->addFunctionCallWithCtx("sensing_keypressed", Compiler::StaticType::Bool, { Compiler::StaticType::String }, { key }); } +CompilerValue *SensingBlocks::compileMouseDown(Compiler *compiler) +{ + return compiler->addFunctionCallWithCtx("sensing_mousedown", Compiler::StaticType::Bool); +} + void SensingBlocks::onAnswer(const std::string &answer) { // https://github.com/scratchfoundation/scratch-vm/blob/6055823f203a696165084b873e661713806583ec/src/blocks/scratch3_sensing.js#L99-L115 @@ -328,3 +334,8 @@ extern "C" bool sensing_keypressed(ExecutionContext *ctx, const StringPtr *key) std::string u8name = utf8::utf16to8(std::u16string(key->data)); return ctx->engine()->keyPressed(u8name); } + +extern "C" bool sensing_mousedown(ExecutionContext *ctx) +{ + return ctx->engine()->mousePressed(); +} diff --git a/src/blocks/sensingblocks.h b/src/blocks/sensingblocks.h index 1bebf79bc..35dd12d7e 100644 --- a/src/blocks/sensingblocks.h +++ b/src/blocks/sensingblocks.h @@ -49,6 +49,7 @@ class SensingBlocks : public IExtension static CompilerValue *compileAskAndWait(Compiler *compiler); static CompilerValue *compileAnswer(Compiler *compiler); static CompilerValue *compileKeyPressed(Compiler *compiler); + static CompilerValue *compileMouseDown(Compiler *compiler); static void onAnswer(const std::string &answer); static void enqueueAsk(const std::string &question, Thread *thread); diff --git a/test/blocks/sensing_blocks_test.cpp b/test/blocks/sensing_blocks_test.cpp index ad95fb727..ccbd4601a 100644 --- a/test/blocks/sensing_blocks_test.cpp +++ b/test/blocks/sensing_blocks_test.cpp @@ -1229,3 +1229,29 @@ TEST_F(SensingBlocksTest, KeyPressed_M) ASSERT_FALSE(value_toBool(&value)); value_free(&value); } + +TEST_F(SensingBlocksTest, MouseDown) +{ + auto targetMock = std::make_shared(); + targetMock->setEngine(&m_engineMock); + + ScriptBuilder builder(m_extension.get(), m_engine, targetMock); + builder.addBlock("sensing_mousedown"); + Block *block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, targetMock.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(targetMock.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(targetMock.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, mousePressed()).WillOnce(Return(true)); + ValueData value = thread.runReporter(); + ASSERT_TRUE(value_toBool(&value)); + value_free(&value); + + EXPECT_CALL(m_engineMock, mousePressed()).WillOnce(Return(false)); + value = thread.runReporter(); + ASSERT_FALSE(value_toBool(&value)); + value_free(&value); +} From 22ef673779085c376b4cffe33b359a3456352f02 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sun, 10 Aug 2025 09:55:09 +0200 Subject: [PATCH 10/20] Implement sensing_mousex block --- src/blocks/sensingblocks.cpp | 11 +++++++++++ src/blocks/sensingblocks.h | 1 + test/blocks/sensing_blocks_test.cpp | 21 +++++++++++++++++++++ 3 files changed, 33 insertions(+) diff --git a/src/blocks/sensingblocks.cpp b/src/blocks/sensingblocks.cpp index 762147e66..25931489e 100644 --- a/src/blocks/sensingblocks.cpp +++ b/src/blocks/sensingblocks.cpp @@ -44,6 +44,7 @@ void SensingBlocks::registerBlocks(IEngine *engine) engine->addCompileFunction(this, "sensing_answer", &compileAnswer); engine->addCompileFunction(this, "sensing_keypressed", &compileKeyPressed); engine->addCompileFunction(this, "sensing_mousedown", &compileMouseDown); + engine->addCompileFunction(this, "sensing_mousex", &compileMouseX); } void SensingBlocks::onInit(IEngine *engine) @@ -173,6 +174,11 @@ CompilerValue *SensingBlocks::compileMouseDown(Compiler *compiler) return compiler->addFunctionCallWithCtx("sensing_mousedown", Compiler::StaticType::Bool); } +CompilerValue *SensingBlocks::compileMouseX(Compiler *compiler) +{ + return compiler->addFunctionCallWithCtx("sensing_mousex", Compiler::StaticType::Number); +} + void SensingBlocks::onAnswer(const std::string &answer) { // https://github.com/scratchfoundation/scratch-vm/blob/6055823f203a696165084b873e661713806583ec/src/blocks/scratch3_sensing.js#L99-L115 @@ -339,3 +345,8 @@ extern "C" bool sensing_mousedown(ExecutionContext *ctx) { return ctx->engine()->mousePressed(); } + +extern "C" double sensing_mousex(ExecutionContext *ctx) +{ + return ctx->engine()->mouseX(); +} diff --git a/src/blocks/sensingblocks.h b/src/blocks/sensingblocks.h index 35dd12d7e..4269428b3 100644 --- a/src/blocks/sensingblocks.h +++ b/src/blocks/sensingblocks.h @@ -50,6 +50,7 @@ class SensingBlocks : public IExtension static CompilerValue *compileAnswer(Compiler *compiler); static CompilerValue *compileKeyPressed(Compiler *compiler); static CompilerValue *compileMouseDown(Compiler *compiler); + static CompilerValue *compileMouseX(Compiler *compiler); static void onAnswer(const std::string &answer); static void enqueueAsk(const std::string &question, Thread *thread); diff --git a/test/blocks/sensing_blocks_test.cpp b/test/blocks/sensing_blocks_test.cpp index ccbd4601a..72ef4b3e1 100644 --- a/test/blocks/sensing_blocks_test.cpp +++ b/test/blocks/sensing_blocks_test.cpp @@ -1255,3 +1255,24 @@ TEST_F(SensingBlocksTest, MouseDown) ASSERT_FALSE(value_toBool(&value)); value_free(&value); } + +TEST_F(SensingBlocksTest, MouseX) +{ + auto targetMock = std::make_shared(); + targetMock->setEngine(&m_engineMock); + + ScriptBuilder builder(m_extension.get(), m_engine, targetMock); + builder.addBlock("sensing_mousex"); + Block *block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, targetMock.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(targetMock.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(targetMock.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, mouseX()).WillOnce(Return(53.7)); + ValueData value = thread.runReporter(); + ASSERT_EQ(value_toDouble(&value), 53.7); + value_free(&value); +} From 121d00d740a62937d6c14fec923e3c22b91ce47e Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sun, 10 Aug 2025 10:00:27 +0200 Subject: [PATCH 11/20] Implement sensing_mousey block --- src/blocks/sensingblocks.cpp | 11 +++++++++++ src/blocks/sensingblocks.h | 1 + test/blocks/sensing_blocks_test.cpp | 21 +++++++++++++++++++++ 3 files changed, 33 insertions(+) diff --git a/src/blocks/sensingblocks.cpp b/src/blocks/sensingblocks.cpp index 25931489e..6ca1044d4 100644 --- a/src/blocks/sensingblocks.cpp +++ b/src/blocks/sensingblocks.cpp @@ -45,6 +45,7 @@ void SensingBlocks::registerBlocks(IEngine *engine) engine->addCompileFunction(this, "sensing_keypressed", &compileKeyPressed); engine->addCompileFunction(this, "sensing_mousedown", &compileMouseDown); engine->addCompileFunction(this, "sensing_mousex", &compileMouseX); + engine->addCompileFunction(this, "sensing_mousey", &compileMouseY); } void SensingBlocks::onInit(IEngine *engine) @@ -179,6 +180,11 @@ CompilerValue *SensingBlocks::compileMouseX(Compiler *compiler) return compiler->addFunctionCallWithCtx("sensing_mousex", Compiler::StaticType::Number); } +CompilerValue *SensingBlocks::compileMouseY(Compiler *compiler) +{ + return compiler->addFunctionCallWithCtx("sensing_mousey", Compiler::StaticType::Number); +} + void SensingBlocks::onAnswer(const std::string &answer) { // https://github.com/scratchfoundation/scratch-vm/blob/6055823f203a696165084b873e661713806583ec/src/blocks/scratch3_sensing.js#L99-L115 @@ -350,3 +356,8 @@ extern "C" double sensing_mousex(ExecutionContext *ctx) { return ctx->engine()->mouseX(); } + +extern "C" double sensing_mousey(ExecutionContext *ctx) +{ + return ctx->engine()->mouseY(); +} diff --git a/src/blocks/sensingblocks.h b/src/blocks/sensingblocks.h index 4269428b3..f1260a890 100644 --- a/src/blocks/sensingblocks.h +++ b/src/blocks/sensingblocks.h @@ -51,6 +51,7 @@ class SensingBlocks : public IExtension static CompilerValue *compileKeyPressed(Compiler *compiler); static CompilerValue *compileMouseDown(Compiler *compiler); static CompilerValue *compileMouseX(Compiler *compiler); + static CompilerValue *compileMouseY(Compiler *compiler); static void onAnswer(const std::string &answer); static void enqueueAsk(const std::string &question, Thread *thread); diff --git a/test/blocks/sensing_blocks_test.cpp b/test/blocks/sensing_blocks_test.cpp index 72ef4b3e1..ef1b70dd6 100644 --- a/test/blocks/sensing_blocks_test.cpp +++ b/test/blocks/sensing_blocks_test.cpp @@ -1276,3 +1276,24 @@ TEST_F(SensingBlocksTest, MouseX) ASSERT_EQ(value_toDouble(&value), 53.7); value_free(&value); } + +TEST_F(SensingBlocksTest, MouseY) +{ + auto targetMock = std::make_shared(); + targetMock->setEngine(&m_engineMock); + + ScriptBuilder builder(m_extension.get(), m_engine, targetMock); + builder.addBlock("sensing_mousey"); + Block *block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, targetMock.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(targetMock.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(targetMock.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, mouseY()).WillOnce(Return(-78.21)); + ValueData value = thread.runReporter(); + ASSERT_EQ(value_toDouble(&value), -78.21); + value_free(&value); +} From d624dc08e08243aadf44d80f76b34b32feb87b8e Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sun, 10 Aug 2025 10:12:54 +0200 Subject: [PATCH 12/20] Implement sensing_setdragmode block --- src/blocks/sensingblocks.cpp | 29 +++++++++++++ src/blocks/sensingblocks.h | 1 + test/blocks/sensing_blocks_test.cpp | 65 +++++++++++++++++++++++++++++ 3 files changed, 95 insertions(+) diff --git a/src/blocks/sensingblocks.cpp b/src/blocks/sensingblocks.cpp index 6ca1044d4..b097032b9 100644 --- a/src/blocks/sensingblocks.cpp +++ b/src/blocks/sensingblocks.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -46,6 +47,7 @@ void SensingBlocks::registerBlocks(IEngine *engine) engine->addCompileFunction(this, "sensing_mousedown", &compileMouseDown); engine->addCompileFunction(this, "sensing_mousex", &compileMouseX); engine->addCompileFunction(this, "sensing_mousey", &compileMouseY); + engine->addCompileFunction(this, "sensing_setdragmode", &compileSetDragMode); } void SensingBlocks::onInit(IEngine *engine) @@ -185,6 +187,28 @@ CompilerValue *SensingBlocks::compileMouseY(Compiler *compiler) return compiler->addFunctionCallWithCtx("sensing_mousey", Compiler::StaticType::Number); } +CompilerValue *SensingBlocks::compileSetDragMode(Compiler *compiler) +{ + if (compiler->target()->isStage()) + return nullptr; + + Field *field = compiler->field("DRAG_MODE"); + assert(field); + + std::string mode = field->value().toString(); + CompilerValue *draggable = nullptr; + + if (mode == "draggable") + draggable = compiler->addConstValue(true); + else if (mode == "not draggable") + draggable = compiler->addConstValue(false); + else + return nullptr; + + compiler->addTargetFunctionCall("sensing_setdragmode", Compiler::StaticType::Void, { Compiler::StaticType::Bool }, { draggable }); + return nullptr; +} + void SensingBlocks::onAnswer(const std::string &answer) { // https://github.com/scratchfoundation/scratch-vm/blob/6055823f203a696165084b873e661713806583ec/src/blocks/scratch3_sensing.js#L99-L115 @@ -361,3 +385,8 @@ extern "C" double sensing_mousey(ExecutionContext *ctx) { return ctx->engine()->mouseY(); } + +extern "C" void sensing_setdragmode(Sprite *sprite, bool draggable) +{ + sprite->setDraggable(draggable); +} diff --git a/src/blocks/sensingblocks.h b/src/blocks/sensingblocks.h index f1260a890..73b5defe5 100644 --- a/src/blocks/sensingblocks.h +++ b/src/blocks/sensingblocks.h @@ -52,6 +52,7 @@ class SensingBlocks : public IExtension static CompilerValue *compileMouseDown(Compiler *compiler); static CompilerValue *compileMouseX(Compiler *compiler); static CompilerValue *compileMouseY(Compiler *compiler); + static CompilerValue *compileSetDragMode(Compiler *compiler); static void onAnswer(const std::string &answer); static void enqueueAsk(const std::string &question, Thread *thread); diff --git a/test/blocks/sensing_blocks_test.cpp b/test/blocks/sensing_blocks_test.cpp index ef1b70dd6..dfa966034 100644 --- a/test/blocks/sensing_blocks_test.cpp +++ b/test/blocks/sensing_blocks_test.cpp @@ -1297,3 +1297,68 @@ TEST_F(SensingBlocksTest, MouseY) ASSERT_EQ(value_toDouble(&value), -78.21); value_free(&value); } + +TEST_F(SensingBlocksTest, SetDragMode_Draggable) +{ + auto sprite = std::make_shared(); + sprite->setDraggable(false); + + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + builder.addBlock("sensing_setdragmode"); + builder.addDropdownField("DRAG_MODE", "draggable"); + builder.build(); + + builder.run(); + ASSERT_TRUE(sprite->draggable()); + + builder.run(); + ASSERT_TRUE(sprite->draggable()); +} + +TEST_F(SensingBlocksTest, SetDragMode_NotDraggable) +{ + auto sprite = std::make_shared(); + sprite->setDraggable(true); + + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + builder.addBlock("sensing_setdragmode"); + builder.addDropdownField("DRAG_MODE", "not draggable"); + builder.build(); + + builder.run(); + ASSERT_FALSE(sprite->draggable()); + + builder.run(); + ASSERT_FALSE(sprite->draggable()); +} + +TEST_F(SensingBlocksTest, SetDragMode_Invalid) +{ + auto sprite = std::make_shared(); + sprite->setDraggable(true); + + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + builder.addBlock("sensing_setdragmode"); + builder.addDropdownField("DRAG_MODE", "lorem ipsum"); + builder.build(); + + builder.run(); + ASSERT_TRUE(sprite->draggable()); + + sprite->setDraggable(false); + + builder.run(); + ASSERT_FALSE(sprite->draggable()); +} + +TEST_F(SensingBlocksTest, SetDragMode_Stage) +{ + auto stage = std::make_shared(); + + ScriptBuilder builder(m_extension.get(), m_engine, stage); + builder.addBlock("sensing_setdragmode"); + builder.addDropdownField("DRAG_MODE", "draggable"); + builder.build(); + + builder.run(); +} From 25686e64d309d54dc09d5c647e57905d21212e5c Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sun, 10 Aug 2025 10:59:57 +0200 Subject: [PATCH 13/20] Implement sensing_loudness block --- src/blocks/sensingblocks.cpp | 17 ++++++++++++++ src/blocks/sensingblocks.h | 4 ++++ test/blocks/sensing_blocks_test.cpp | 36 ++++++++++++++++++++++++++++- 3 files changed, 56 insertions(+), 1 deletion(-) diff --git a/src/blocks/sensingblocks.cpp b/src/blocks/sensingblocks.cpp index b097032b9..f7e14529d 100644 --- a/src/blocks/sensingblocks.cpp +++ b/src/blocks/sensingblocks.cpp @@ -17,6 +17,8 @@ #include #include "sensingblocks.h" +#include "audio/audioinput.h" +#include "audio/iaudioloudness.h" using namespace libscratchcpp; @@ -48,6 +50,7 @@ void SensingBlocks::registerBlocks(IEngine *engine) engine->addCompileFunction(this, "sensing_mousex", &compileMouseX); engine->addCompileFunction(this, "sensing_mousey", &compileMouseY); engine->addCompileFunction(this, "sensing_setdragmode", &compileSetDragMode); + engine->addCompileFunction(this, "sensing_loudness", &compileLoudness); } void SensingBlocks::onInit(IEngine *engine) @@ -209,6 +212,11 @@ CompilerValue *SensingBlocks::compileSetDragMode(Compiler *compiler) return nullptr; } +CompilerValue *SensingBlocks::compileLoudness(Compiler *compiler) +{ + return compiler->addFunctionCall("sensing_loudness", Compiler::StaticType::Number); +} + void SensingBlocks::onAnswer(const std::string &answer) { // https://github.com/scratchfoundation/scratch-vm/blob/6055823f203a696165084b873e661713806583ec/src/blocks/scratch3_sensing.js#L99-L115 @@ -390,3 +398,12 @@ extern "C" void sensing_setdragmode(Sprite *sprite, bool draggable) { sprite->setDraggable(draggable); } + +extern "C" double sensing_loudness() +{ + if (!SensingBlocks::audioInput) + SensingBlocks::audioInput = AudioInput::instance().get(); + + auto audioLoudness = SensingBlocks::audioInput->audioLoudness(); + return audioLoudness->getLoudness(); +} diff --git a/src/blocks/sensingblocks.h b/src/blocks/sensingblocks.h index 73b5defe5..eaf2ec461 100644 --- a/src/blocks/sensingblocks.h +++ b/src/blocks/sensingblocks.h @@ -9,6 +9,7 @@ namespace libscratchcpp { +class IAudioInput; class ExecutionContext; class Thread; @@ -25,6 +26,8 @@ class SensingBlocks : public IExtension static void clearQuestions(); static void askQuestion(ExecutionContext *ctx, const StringPtr *question); + static inline IAudioInput *audioInput = nullptr; + private: struct Question { @@ -53,6 +56,7 @@ class SensingBlocks : public IExtension static CompilerValue *compileMouseX(Compiler *compiler); static CompilerValue *compileMouseY(Compiler *compiler); static CompilerValue *compileSetDragMode(Compiler *compiler); + static CompilerValue *compileLoudness(Compiler *compiler); static void onAnswer(const std::string &answer); static void enqueueAsk(const std::string &question, Thread *thread); diff --git a/test/blocks/sensing_blocks_test.cpp b/test/blocks/sensing_blocks_test.cpp index dfa966034..1623704d9 100644 --- a/test/blocks/sensing_blocks_test.cpp +++ b/test/blocks/sensing_blocks_test.cpp @@ -9,6 +9,8 @@ #include #include #include +#include +#include #include "../common.h" #include "blocks/sensingblocks.h" @@ -36,14 +38,26 @@ class SensingBlocksTest : public testing::Test m_extension->registerBlocks(m_engine); m_extension->onInit(m_engine); registerBlocks(m_engine, m_extension.get()); + + m_audioLoudness = std::make_shared(); + SensingBlocks::audioInput = &m_audioInput; + EXPECT_CALL(m_audioInput, audioLoudness()).WillRepeatedly(Return(m_audioLoudness)); } - void TearDown() override { SensingBlocks::clearQuestions(); } + void TearDown() override + { + SensingBlocks::clearQuestions(); + SensingBlocks::audioInput = nullptr; + } std::unique_ptr m_extension; Project m_project; IEngine *m_engine = nullptr; EngineMock m_engineMock; + std::shared_ptr m_audioLoudness; + + private: + AudioInputMock m_audioInput; }; TEST_F(SensingBlocksTest, TouchingObject_Sprite_CompileTime) @@ -1362,3 +1376,23 @@ TEST_F(SensingBlocksTest, SetDragMode_Stage) builder.run(); } + +TEST_F(SensingBlocksTest, Loudness) +{ + auto targetMock = std::make_shared(); + + ScriptBuilder builder(m_extension.get(), m_engine, targetMock); + builder.addBlock("sensing_loudness"); + Block *block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, targetMock.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(targetMock.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(targetMock.get(), &m_engineMock, &script); + + EXPECT_CALL(*m_audioLoudness, getLoudness()).WillOnce(Return(62)); + ValueData value = thread.runReporter(); + ASSERT_EQ(value_toDouble(&value), 62); + value_free(&value); +} From c43ee3a9e003d1702e18a7ebabadaae0228c6c17 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sun, 10 Aug 2025 11:07:58 +0200 Subject: [PATCH 14/20] Implement sensing_loud block --- src/blocks/sensingblocks.cpp | 8 ++++ src/blocks/sensingblocks.h | 1 + test/blocks/sensing_blocks_test.cpp | 60 +++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+) diff --git a/src/blocks/sensingblocks.cpp b/src/blocks/sensingblocks.cpp index f7e14529d..4a5ee559a 100644 --- a/src/blocks/sensingblocks.cpp +++ b/src/blocks/sensingblocks.cpp @@ -51,6 +51,7 @@ void SensingBlocks::registerBlocks(IEngine *engine) engine->addCompileFunction(this, "sensing_mousey", &compileMouseY); engine->addCompileFunction(this, "sensing_setdragmode", &compileSetDragMode); engine->addCompileFunction(this, "sensing_loudness", &compileLoudness); + engine->addCompileFunction(this, "sensing_loud", &compileLoud); } void SensingBlocks::onInit(IEngine *engine) @@ -217,6 +218,13 @@ CompilerValue *SensingBlocks::compileLoudness(Compiler *compiler) return compiler->addFunctionCall("sensing_loudness", Compiler::StaticType::Number); } +CompilerValue *SensingBlocks::compileLoud(Compiler *compiler) +{ + CompilerValue *treshold = compiler->addConstValue(10); + CompilerValue *loudness = compiler->addFunctionCall("sensing_loudness", Compiler::StaticType::Number); + return compiler->createCmpGT(loudness, treshold); +} + void SensingBlocks::onAnswer(const std::string &answer) { // https://github.com/scratchfoundation/scratch-vm/blob/6055823f203a696165084b873e661713806583ec/src/blocks/scratch3_sensing.js#L99-L115 diff --git a/src/blocks/sensingblocks.h b/src/blocks/sensingblocks.h index eaf2ec461..59a45939c 100644 --- a/src/blocks/sensingblocks.h +++ b/src/blocks/sensingblocks.h @@ -57,6 +57,7 @@ class SensingBlocks : public IExtension static CompilerValue *compileMouseY(Compiler *compiler); static CompilerValue *compileSetDragMode(Compiler *compiler); static CompilerValue *compileLoudness(Compiler *compiler); + static CompilerValue *compileLoud(Compiler *compiler); static void onAnswer(const std::string &answer); static void enqueueAsk(const std::string &question, Thread *thread); diff --git a/test/blocks/sensing_blocks_test.cpp b/test/blocks/sensing_blocks_test.cpp index 1623704d9..dae6e8fa1 100644 --- a/test/blocks/sensing_blocks_test.cpp +++ b/test/blocks/sensing_blocks_test.cpp @@ -1396,3 +1396,63 @@ TEST_F(SensingBlocksTest, Loudness) ASSERT_EQ(value_toDouble(&value), 62); value_free(&value); } + +TEST_F(SensingBlocksTest, Loud_Below) +{ + auto targetMock = std::make_shared(); + + ScriptBuilder builder(m_extension.get(), m_engine, targetMock); + builder.addBlock("sensing_loud"); + Block *block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, targetMock.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(targetMock.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(targetMock.get(), &m_engineMock, &script); + + EXPECT_CALL(*m_audioLoudness, getLoudness()).WillOnce(Return(9)); + ValueData value = thread.runReporter(); + ASSERT_FALSE(value_toBool(&value)); + value_free(&value); +} + +TEST_F(SensingBlocksTest, Loud_Equal) +{ + auto targetMock = std::make_shared(); + + ScriptBuilder builder(m_extension.get(), m_engine, targetMock); + builder.addBlock("sensing_loud"); + Block *block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, targetMock.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(targetMock.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(targetMock.get(), &m_engineMock, &script); + + EXPECT_CALL(*m_audioLoudness, getLoudness()).WillOnce(Return(10)); + ValueData value = thread.runReporter(); + ASSERT_FALSE(value_toBool(&value)); + value_free(&value); +} + +TEST_F(SensingBlocksTest, Loud_Above) +{ + auto targetMock = std::make_shared(); + + ScriptBuilder builder(m_extension.get(), m_engine, targetMock); + builder.addBlock("sensing_loud"); + Block *block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, targetMock.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(targetMock.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(targetMock.get(), &m_engineMock, &script); + + EXPECT_CALL(*m_audioLoudness, getLoudness()).WillOnce(Return(11)); + ValueData value = thread.runReporter(); + ASSERT_TRUE(value_toBool(&value)); + value_free(&value); +} From 2e5d4dc6e3d29373c7e424d1ced6b347ef45a439 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sun, 10 Aug 2025 11:24:47 +0200 Subject: [PATCH 15/20] Implement sensing_timer block --- src/blocks/sensingblocks.cpp | 14 ++++++++++++++ src/blocks/sensingblocks.h | 1 + test/blocks/sensing_blocks_test.cpp | 24 ++++++++++++++++++++++++ 3 files changed, 39 insertions(+) diff --git a/src/blocks/sensingblocks.cpp b/src/blocks/sensingblocks.cpp index 4a5ee559a..4a3f3206d 100644 --- a/src/blocks/sensingblocks.cpp +++ b/src/blocks/sensingblocks.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include "sensingblocks.h" @@ -52,6 +53,7 @@ void SensingBlocks::registerBlocks(IEngine *engine) engine->addCompileFunction(this, "sensing_setdragmode", &compileSetDragMode); engine->addCompileFunction(this, "sensing_loudness", &compileLoudness); engine->addCompileFunction(this, "sensing_loud", &compileLoud); + engine->addCompileFunction(this, "sensing_timer", &compileTimer); } void SensingBlocks::onInit(IEngine *engine) @@ -225,6 +227,13 @@ CompilerValue *SensingBlocks::compileLoud(Compiler *compiler) return compiler->createCmpGT(loudness, treshold); } +CompilerValue *SensingBlocks::compileTimer(Compiler *compiler) +{ + ITimer *timer = compiler->engine()->timer(); + CompilerValue *timerPtr = compiler->addConstValue(timer); + return compiler->addFunctionCall("sensing_timer", Compiler::StaticType::Number, { Compiler::StaticType::Pointer }, { timerPtr }); +} + void SensingBlocks::onAnswer(const std::string &answer) { // https://github.com/scratchfoundation/scratch-vm/blob/6055823f203a696165084b873e661713806583ec/src/blocks/scratch3_sensing.js#L99-L115 @@ -415,3 +424,8 @@ extern "C" double sensing_loudness() auto audioLoudness = SensingBlocks::audioInput->audioLoudness(); return audioLoudness->getLoudness(); } + +extern "C" double sensing_timer(ITimer *timer) +{ + return timer->value(); +} diff --git a/src/blocks/sensingblocks.h b/src/blocks/sensingblocks.h index 59a45939c..aa35f8f28 100644 --- a/src/blocks/sensingblocks.h +++ b/src/blocks/sensingblocks.h @@ -58,6 +58,7 @@ class SensingBlocks : public IExtension static CompilerValue *compileSetDragMode(Compiler *compiler); static CompilerValue *compileLoudness(Compiler *compiler); static CompilerValue *compileLoud(Compiler *compiler); + static CompilerValue *compileTimer(Compiler *compiler); static void onAnswer(const std::string &answer); static void enqueueAsk(const std::string &question, Thread *thread); diff --git a/test/blocks/sensing_blocks_test.cpp b/test/blocks/sensing_blocks_test.cpp index dae6e8fa1..d6e68ffae 100644 --- a/test/blocks/sensing_blocks_test.cpp +++ b/test/blocks/sensing_blocks_test.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include "../common.h" #include "blocks/sensingblocks.h" @@ -1456,3 +1457,26 @@ TEST_F(SensingBlocksTest, Loud_Above) ASSERT_TRUE(value_toBool(&value)); value_free(&value); } + +TEST_F(SensingBlocksTest, Timer) +{ + auto targetMock = std::make_shared(); + + TimerMock timer; + EXPECT_CALL(m_engineMock, timer()).WillOnce(Return(&timer)); + + ScriptBuilder builder(m_extension.get(), m_engine, targetMock); + builder.addBlock("sensing_timer"); + Block *block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, targetMock.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(targetMock.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(targetMock.get(), &m_engineMock, &script); + + EXPECT_CALL(timer, value()).WillOnce(Return(23.4)); + ValueData value = thread.runReporter(); + ASSERT_EQ(value_toDouble(&value), 23.4); + value_free(&value); +} From 3709135559a0d8756e907527df2800e17c9fa95e Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sun, 10 Aug 2025 11:32:28 +0200 Subject: [PATCH 16/20] Implement sensing_resettimer block --- src/blocks/sensingblocks.cpp | 14 ++++++++++++++ src/blocks/sensingblocks.h | 1 + test/blocks/sensing_blocks_test.cpp | 21 +++++++++++++++++++++ 3 files changed, 36 insertions(+) diff --git a/src/blocks/sensingblocks.cpp b/src/blocks/sensingblocks.cpp index 4a3f3206d..336adfc31 100644 --- a/src/blocks/sensingblocks.cpp +++ b/src/blocks/sensingblocks.cpp @@ -54,6 +54,7 @@ void SensingBlocks::registerBlocks(IEngine *engine) engine->addCompileFunction(this, "sensing_loudness", &compileLoudness); engine->addCompileFunction(this, "sensing_loud", &compileLoud); engine->addCompileFunction(this, "sensing_timer", &compileTimer); + engine->addCompileFunction(this, "sensing_resettimer", &compileResetTimer); } void SensingBlocks::onInit(IEngine *engine) @@ -234,6 +235,14 @@ CompilerValue *SensingBlocks::compileTimer(Compiler *compiler) return compiler->addFunctionCall("sensing_timer", Compiler::StaticType::Number, { Compiler::StaticType::Pointer }, { timerPtr }); } +CompilerValue *SensingBlocks::compileResetTimer(Compiler *compiler) +{ + ITimer *timer = compiler->engine()->timer(); + CompilerValue *timerPtr = compiler->addConstValue(timer); + compiler->addFunctionCall("sensing_resettimer", Compiler::StaticType::Void, { Compiler::StaticType::Pointer }, { timerPtr }); + return nullptr; +} + void SensingBlocks::onAnswer(const std::string &answer) { // https://github.com/scratchfoundation/scratch-vm/blob/6055823f203a696165084b873e661713806583ec/src/blocks/scratch3_sensing.js#L99-L115 @@ -429,3 +438,8 @@ extern "C" double sensing_timer(ITimer *timer) { return timer->value(); } + +extern "C" void sensing_resettimer(ITimer *timer) +{ + timer->reset(); +} diff --git a/src/blocks/sensingblocks.h b/src/blocks/sensingblocks.h index aa35f8f28..9d6ca94d3 100644 --- a/src/blocks/sensingblocks.h +++ b/src/blocks/sensingblocks.h @@ -59,6 +59,7 @@ class SensingBlocks : public IExtension static CompilerValue *compileLoudness(Compiler *compiler); static CompilerValue *compileLoud(Compiler *compiler); static CompilerValue *compileTimer(Compiler *compiler); + static CompilerValue *compileResetTimer(Compiler *compiler); static void onAnswer(const std::string &answer); static void enqueueAsk(const std::string &question, Thread *thread); diff --git a/test/blocks/sensing_blocks_test.cpp b/test/blocks/sensing_blocks_test.cpp index d6e68ffae..41a12344f 100644 --- a/test/blocks/sensing_blocks_test.cpp +++ b/test/blocks/sensing_blocks_test.cpp @@ -1480,3 +1480,24 @@ TEST_F(SensingBlocksTest, Timer) ASSERT_EQ(value_toDouble(&value), 23.4); value_free(&value); } + +TEST_F(SensingBlocksTest, ResetTimer) +{ + auto targetMock = std::make_shared(); + + TimerMock timer; + EXPECT_CALL(m_engineMock, timer()).WillOnce(Return(&timer)); + + ScriptBuilder builder(m_extension.get(), m_engine, targetMock); + builder.addBlock("sensing_resettimer"); + Block *block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, targetMock.get()); + auto code = compiler.compile(block); + Script script(targetMock.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(targetMock.get(), &m_engineMock, &script); + + EXPECT_CALL(timer, reset()); + thread.run(); +} From 34ce8b7eb99531309a1b034a7b095be93936f05b Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sun, 10 Aug 2025 17:08:46 +0200 Subject: [PATCH 17/20] Add support for unknown function return type --- .../internal/llvm/instructions/functions.cpp | 10 +++++--- src/engine/internal/llvm/llvmbuildutils.cpp | 7 ++++-- src/engine/internal/llvm/llvmbuildutils.h | 2 +- src/engine/internal/llvm/llvmcodebuilder.cpp | 5 +++- test/llvm/llvmcodebuilder_test.cpp | 25 +++++++++++++++++++ test/llvm/testfunctions.cpp | 8 ++++++ 6 files changed, 50 insertions(+), 7 deletions(-) diff --git a/src/engine/internal/llvm/instructions/functions.cpp b/src/engine/internal/llvm/instructions/functions.cpp index cc5c9679d..1bdb379b7 100644 --- a/src/engine/internal/llvm/instructions/functions.cpp +++ b/src/engine/internal/llvm/instructions/functions.cpp @@ -46,15 +46,19 @@ LLVMInstruction *Functions::buildFunctionCall(LLVMInstruction *ins) // Args for (auto &arg : ins->args) { - types.push_back(m_utils.getType(arg.first)); + types.push_back(m_utils.getType(arg.first, false)); args.push_back(m_utils.castValue(arg.second, arg.first)); } - llvm::Type *retType = m_utils.getType(ins->functionReturnReg ? ins->functionReturnReg->type() : Compiler::StaticType::Void); + llvm::Type *retType = m_utils.getType(ins->functionReturnReg ? ins->functionReturnReg->type() : Compiler::StaticType::Void, true); llvm::Value *ret = m_builder.CreateCall(m_utils.functions().resolveFunction(ins->functionName, llvm::FunctionType::get(retType, types, false)), args); if (ins->functionReturnReg) { - ins->functionReturnReg->value = ret; + if (ins->functionReturnReg->type() == Compiler::StaticType::Unknown) { + ins->functionReturnReg->value = m_utils.addAlloca(retType); + m_builder.CreateStore(ret, ins->functionReturnReg->value); + } else + ins->functionReturnReg->value = ret; if (ins->functionReturnReg->type() == Compiler::StaticType::String) m_utils.freeStringLater(ins->functionReturnReg->value); diff --git a/src/engine/internal/llvm/llvmbuildutils.cpp b/src/engine/internal/llvm/llvmbuildutils.cpp index d1b78ecea..771bb6659 100644 --- a/src/engine/internal/llvm/llvmbuildutils.cpp +++ b/src/engine/internal/llvm/llvmbuildutils.cpp @@ -496,7 +496,7 @@ llvm::Value *LLVMBuildUtils::castValue(LLVMRegister *reg, Compiler::StaticType t } } -llvm::Type *LLVMBuildUtils::getType(Compiler::StaticType type) +llvm::Type *LLVMBuildUtils::getType(Compiler::StaticType type, bool isReturnType) { switch (type) { case Compiler::StaticType::Void: @@ -515,7 +515,10 @@ llvm::Type *LLVMBuildUtils::getType(Compiler::StaticType type) return m_builder.getVoidTy()->getPointerTo(); case Compiler::StaticType::Unknown: - return m_valueDataType->getPointerTo(); + if (isReturnType) + return m_valueDataType; + else + return m_valueDataType->getPointerTo(); default: assert(false); diff --git a/src/engine/internal/llvm/llvmbuildutils.h b/src/engine/internal/llvm/llvmbuildutils.h index 9ca28a1ef..6bfeed3c0 100644 --- a/src/engine/internal/llvm/llvmbuildutils.h +++ b/src/engine/internal/llvm/llvmbuildutils.h @@ -80,7 +80,7 @@ class LLVMBuildUtils llvm::Value *addAlloca(llvm::Type *type); llvm::Value *castValue(LLVMRegister *reg, Compiler::StaticType targetType); - llvm::Type *getType(Compiler::StaticType type); + llvm::Type *getType(Compiler::StaticType type, bool isReturnType); llvm::Value *isNaN(llvm::Value *num); llvm::Value *removeNaN(llvm::Value *num); diff --git a/src/engine/internal/llvm/llvmcodebuilder.cpp b/src/engine/internal/llvm/llvmcodebuilder.cpp index e5cc10a10..b189905fc 100644 --- a/src/engine/internal/llvm/llvmcodebuilder.cpp +++ b/src/engine/internal/llvm/llvmcodebuilder.cpp @@ -100,7 +100,10 @@ CompilerValue *LLVMCodeBuilder::addFunctionCall(const std::string &functionName, if (returnType != Compiler::StaticType::Void) { auto reg = std::make_shared(returnType); - reg->isRawValue = true; + + if (returnType != Compiler::StaticType::Unknown) + reg->isRawValue = true; + ins->functionReturnReg = reg.get(); return addReg(reg, ins); } diff --git a/test/llvm/llvmcodebuilder_test.cpp b/test/llvm/llvmcodebuilder_test.cpp index 69a24e345..91e0c4759 100644 --- a/test/llvm/llvmcodebuilder_test.cpp +++ b/test/llvm/llvmcodebuilder_test.cpp @@ -3301,3 +3301,28 @@ TEST_F(LLVMCodeBuilderTest, Reporters) ASSERT_EQ(value_toPointer(&ret), &pointee); value_free(&ret); } + +TEST_F(LLVMCodeBuilderTest, UnknownTypeReporter) +{ + Sprite sprite; + auto var = std::make_shared("", ""); + var->setValue("Hello world!"); + sprite.addVariable(var); + + LLVMCodeBuilder *builder = m_utils.createReporterBuilder(&sprite); + + CompilerValue *num = builder->addConstValue(5.2); + builder->addFunctionCall("test_const_unknown", Compiler::StaticType::Unknown, { Compiler::StaticType::Unknown }, { num }); + + auto code = builder->build(); + + Script script(&sprite, nullptr, nullptr); + script.setCode(code); + Thread thread1(&sprite, nullptr, &script); + auto ctx = code->createExecutionContext(&thread1); + + ValueData ret = code->runReporter(ctx.get()); + ASSERT_TRUE(value_isNumber(&ret)); + ASSERT_EQ(value_toDouble(&ret), 5.2); + value_free(&ret); +} diff --git a/test/llvm/testfunctions.cpp b/test/llvm/testfunctions.cpp index 2295d92e3..b49e701aa 100644 --- a/test/llvm/testfunctions.cpp +++ b/test/llvm/testfunctions.cpp @@ -139,6 +139,14 @@ extern "C" return ret; } + ValueData test_const_unknown(const ValueData *v) + { + ValueData ret; + value_init(&ret); + value_assign_copy(&ret, v); + return ret; + } + const void *test_const_pointer(const void *v) { return v; From 86f1ce0d6e4475fc85e6961bdce4968d088f3f03 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sun, 10 Aug 2025 17:28:08 +0200 Subject: [PATCH 18/20] Implement sensing_of block --- src/blocks/sensingblocks.cpp | 258 +++++++ src/blocks/sensingblocks.h | 1 + test/blocks/sensing_blocks_test.cpp | 1072 ++++++++++++++++++++++++++- 3 files changed, 1330 insertions(+), 1 deletion(-) diff --git a/src/blocks/sensingblocks.cpp b/src/blocks/sensingblocks.cpp index 336adfc31..91ce34188 100644 --- a/src/blocks/sensingblocks.cpp +++ b/src/blocks/sensingblocks.cpp @@ -10,7 +10,10 @@ #include #include #include +#include +#include #include +#include #include #include #include @@ -55,6 +58,7 @@ void SensingBlocks::registerBlocks(IEngine *engine) engine->addCompileFunction(this, "sensing_loud", &compileLoud); engine->addCompileFunction(this, "sensing_timer", &compileTimer); engine->addCompileFunction(this, "sensing_resettimer", &compileResetTimer); + engine->addCompileFunction(this, "sensing_of", &compileOf); } void SensingBlocks::onInit(IEngine *engine) @@ -243,6 +247,101 @@ CompilerValue *SensingBlocks::compileResetTimer(Compiler *compiler) return nullptr; } +CompilerValue *SensingBlocks::compileOf(Compiler *compiler) +{ + IEngine *engine = compiler->engine(); + Input *input = compiler->input("OBJECT"); + Field *field = compiler->field("PROPERTY"); + assert(input); + assert(field); + + std::string property = field->value().toString(); + + if (input->pointsToDropdownMenu()) { + // Compile time + std::string value = input->selectedMenuItem(); + Target *target = nullptr; + CompilerValue *targetPtr = nullptr; + + if (value == "_stage_") { + // Stage properties + target = engine->stage(); + targetPtr = compiler->addConstValue(target); + + if (property == "backdrop #") + return compiler->addFunctionCall("sensing_costume_number_of_target", Compiler::StaticType::Number, { Compiler::StaticType::Pointer }, { targetPtr }); + else if (property == "backdrop name") + return compiler->addFunctionCall("sensing_costume_name_of_target", Compiler::StaticType::String, { Compiler::StaticType::Pointer }, { targetPtr }); + } else { + // Sprite properties + target = engine->targetAt(engine->findTarget(value)); + + if (target) { + targetPtr = compiler->addConstValue(target); + + if (property == "x position") + return compiler->addFunctionCall("sensing_x_position_of_sprite", Compiler::StaticType::Number, { Compiler::StaticType::Pointer }, { targetPtr }); + else if (property == "y position") + return compiler->addFunctionCall("sensing_y_position_of_sprite", Compiler::StaticType::Number, { Compiler::StaticType::Pointer }, { targetPtr }); + else if (property == "direction") + return compiler->addFunctionCall("sensing_direction_of_sprite", Compiler::StaticType::Number, { Compiler::StaticType::Pointer }, { targetPtr }); + else if (property == "costume #") + return compiler->addFunctionCall("sensing_costume_number_of_target", Compiler::StaticType::Number, { Compiler::StaticType::Pointer }, { targetPtr }); + else if (property == "costume name") + return compiler->addFunctionCall("sensing_costume_name_of_target", Compiler::StaticType::String, { Compiler::StaticType::Pointer }, { targetPtr }); + else if (property == "size") + return compiler->addFunctionCall("sensing_size_of_sprite", Compiler::StaticType::Number, { Compiler::StaticType::Pointer }, { targetPtr }); + } + } + + // Common properties + if (target && targetPtr) { + if (property == "volume") + return compiler->addFunctionCall("sensing_volume_of_target", Compiler::StaticType::Number, { Compiler::StaticType::Pointer }, { targetPtr }); + else { + auto var = target->variableAt(target->findVariable(property)); + + if (var) + return compiler->addVariableValue(var.get()); + } + } + } else { + // Runtime + CompilerValue *targetName = compiler->addInput(input); + CompilerValue *targetPtr = compiler->addFunctionCallWithCtx("sensing_get_target", Compiler::StaticType::Pointer, { Compiler::StaticType::String }, { targetName }); + + // Stage properties + if (property == "backdrop #") { + return compiler->addFunctionCall("sensing_backdrop_number_of_stage_with_check", Compiler::StaticType::Number, { Compiler::StaticType::Pointer }, { targetPtr }); + } else if (property == "backdrop name") + return compiler->addFunctionCall("sensing_backdrop_name_of_stage_with_check", Compiler::StaticType::String, { Compiler::StaticType::Pointer }, { targetPtr }); + + // Sprite properties + if (property == "x position") + return compiler->addFunctionCall("sensing_x_position_of_sprite_with_check", Compiler::StaticType::Number, { Compiler::StaticType::Pointer }, { targetPtr }); + else if (property == "y position") + return compiler->addFunctionCall("sensing_y_position_of_sprite_with_check", Compiler::StaticType::Number, { Compiler::StaticType::Pointer }, { targetPtr }); + else if (property == "direction") + return compiler->addFunctionCall("sensing_direction_of_sprite_with_check", Compiler::StaticType::Number, { Compiler::StaticType::Pointer }, { targetPtr }); + else if (property == "costume #") + return compiler->addFunctionCall("sensing_costume_number_of_sprite_with_check", Compiler::StaticType::Number, { Compiler::StaticType::Pointer }, { targetPtr }); + else if (property == "costume name") + return compiler->addFunctionCall("sensing_costume_name_of_sprite_with_check", Compiler::StaticType::String, { Compiler::StaticType::Pointer }, { targetPtr }); + else if (property == "size") + return compiler->addFunctionCall("sensing_size_of_sprite_with_check", Compiler::StaticType::Number, { Compiler::StaticType::Pointer }, { targetPtr }); + + // Common properties + if (property == "volume") + return compiler->addFunctionCall("sensing_volume_of_target_with_check", Compiler::StaticType::Number, { Compiler::StaticType::Pointer }, { targetPtr }); + else { + CompilerValue *varName = compiler->addConstValue(property); + return compiler->addFunctionCall("sensing_variable_of_target", Compiler::StaticType::Unknown, { Compiler::StaticType::Pointer, Compiler::StaticType::String }, { targetPtr, varName }); + } + } + + return compiler->addConstValue(0.0); +} + void SensingBlocks::onAnswer(const std::string &answer) { // https://github.com/scratchfoundation/scratch-vm/blob/6055823f203a696165084b873e661713806583ec/src/blocks/scratch3_sensing.js#L99-L115 @@ -443,3 +542,162 @@ extern "C" void sensing_resettimer(ITimer *timer) { timer->reset(); } + +extern "C" double sensing_x_position_of_sprite(Sprite *sprite) +{ + return sprite->x(); +} + +extern "C" double sensing_y_position_of_sprite(Sprite *sprite) +{ + return sprite->y(); +} + +extern "C" double sensing_direction_of_sprite(Sprite *sprite) +{ + return sprite->direction(); +} + +extern "C" double sensing_costume_number_of_target(Target *target) +{ + return target->costumeIndex() + 1; +} + +extern "C" StringPtr *sensing_costume_name_of_target(Target *target) +{ + const std::string &name = target->currentCostume()->name(); + StringPtr *ret = string_pool_new(); + string_assign_cstring(ret, name.c_str()); + return ret; +} + +extern "C" double sensing_size_of_sprite(Sprite *sprite) +{ + return sprite->size(); +} + +extern "C" double sensing_volume_of_target(Target *target) +{ + return target->volume(); +} + +extern "C" Target *sensing_get_target(ExecutionContext *ctx, const StringPtr *name) +{ + // TODO: Use UTF-16 in engine + std::string u8name = utf8::utf16to8(std::u16string(name->data)); + IEngine *engine = ctx->engine(); + return engine->targetAt(engine->findTarget(u8name)); +} + +extern "C" double sensing_x_position_of_sprite_with_check(Target *target) +{ + if (target && !target->isStage()) { + Sprite *sprite = static_cast(target); + return sprite->x(); + } + + return 0.0; +} + +extern "C" double sensing_y_position_of_sprite_with_check(Target *target) +{ + if (target && !target->isStage()) { + Sprite *sprite = static_cast(target); + return sprite->y(); + } + + return 0.0; +} + +extern "C" double sensing_direction_of_sprite_with_check(Target *target) +{ + if (target && !target->isStage()) { + Sprite *sprite = static_cast(target); + return sprite->direction(); + } + + return 0.0; +} + +extern "C" double sensing_costume_number_of_sprite_with_check(Target *target) +{ + if (target && !target->isStage()) + return target->costumeIndex() + 1; + + return 0.0; +} + +extern "C" StringPtr *sensing_costume_name_of_sprite_with_check(Target *target) +{ + StringPtr *ret = string_pool_new(); + + if (target && !target->isStage()) { + const std::string &name = target->currentCostume()->name(); + string_assign_cstring(ret, name.c_str()); + } else + string_assign_cstring(ret, "0"); + + return ret; +} + +extern "C" double sensing_size_of_sprite_with_check(Target *target) +{ + if (target && !target->isStage()) { + Sprite *sprite = static_cast(target); + return sprite->size(); + } + + return 0.0; +} + +extern "C" double sensing_backdrop_number_of_stage_with_check(Target *target) +{ + if (target && target->isStage()) + return target->costumeIndex() + 1; + + return 0.0; +} + +extern "C" StringPtr *sensing_backdrop_name_of_stage_with_check(Target *target) +{ + StringPtr *ret = string_pool_new(); + + if (target && target->isStage()) { + const std::string &name = target->currentCostume()->name(); + string_assign_cstring(ret, name.c_str()); + } else + string_assign_cstring(ret, ""); + + return ret; +} + +extern "C" double sensing_volume_of_target_with_check(Target *target) +{ + if (target) + return target->volume(); + + return 0.0; +} + +extern "C" ValueData sensing_variable_of_target(Target *target, const StringPtr *varName) +{ + if (target) { + // TODO: Use UTF-16 in... Target? + std::string u8name = utf8::utf16to8(std::u16string(varName->data)); + int varIndex = target->findVariable(u8name); + if (varIndex >= 0) { + auto var = target->variableAt(varIndex); + + if (var) { + ValueData ret; + value_init(&ret); + value_assign_copy(&ret, &var->value().data()); + return ret; + } + } + } + + ValueData ret; + value_init(&ret); + return ret; +} diff --git a/src/blocks/sensingblocks.h b/src/blocks/sensingblocks.h index 9d6ca94d3..2ba254898 100644 --- a/src/blocks/sensingblocks.h +++ b/src/blocks/sensingblocks.h @@ -60,6 +60,7 @@ class SensingBlocks : public IExtension static CompilerValue *compileLoud(Compiler *compiler); static CompilerValue *compileTimer(Compiler *compiler); static CompilerValue *compileResetTimer(Compiler *compiler); + static CompilerValue *compileOf(Compiler *compiler); static void onAnswer(const std::string &answer); static void enqueueAsk(const std::string &question, Thread *thread); diff --git a/test/blocks/sensing_blocks_test.cpp b/test/blocks/sensing_blocks_test.cpp index 41a12344f..d610ebbaa 100644 --- a/test/blocks/sensing_blocks_test.cpp +++ b/test/blocks/sensing_blocks_test.cpp @@ -1,7 +1,8 @@ #include #include #include -#include +#include +#include #include #include #include @@ -1501,3 +1502,1072 @@ TEST_F(SensingBlocksTest, ResetTimer) EXPECT_CALL(timer, reset()); thread.run(); } + +TEST_F(SensingBlocksTest, Of_XPositionOfSprite_CompileTime) +{ + auto targetMock = std::make_shared(); + targetMock->setEngine(&m_engineMock); + + Sprite targetSprite; + targetSprite.setX(65.41); + targetSprite.setY(-56.28); + + ScriptBuilder builder(m_extension.get(), m_engine, targetMock); + builder.addBlock("sensing_of"); + builder.addDropdownField("PROPERTY", "x position"); + builder.addDropdownInput("OBJECT", "Sprite2"); + Block *block = builder.currentBlock(); + + EXPECT_CALL(m_engineMock, findTarget("Sprite2")).WillOnce(Return(4)); + EXPECT_CALL(m_engineMock, targetAt(4)).WillOnce(Return(&targetSprite)); + Compiler compiler(&m_engineMock, targetMock.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(targetMock.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(targetMock.get(), &m_engineMock, &script); + + ValueData value = thread.runReporter(); + ASSERT_EQ(value_toDouble(&value), 65.41); + value_free(&value); +} + +TEST_F(SensingBlocksTest, Of_XPositionOfSprite_Runtime) +{ + auto targetMock = std::make_shared(); + targetMock->setEngine(&m_engineMock); + + Sprite targetSprite; + targetSprite.setX(65.41); + targetSprite.setY(-56.28); + + ScriptBuilder builder(m_extension.get(), m_engine, targetMock); + + builder.addBlock("test_const_string"); + builder.addValueInput("STRING", "Sprite2"); + auto valueBlock = builder.takeBlock(); + + builder.addBlock("sensing_of"); + builder.addDropdownField("PROPERTY", "x position"); + builder.addObscuredInput("OBJECT", valueBlock); + Block *block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, targetMock.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(targetMock.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(targetMock.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, findTarget("Sprite2")).WillOnce(Return(4)); + EXPECT_CALL(m_engineMock, targetAt(4)).WillOnce(Return(&targetSprite)); + ValueData value = thread.runReporter(); + ASSERT_EQ(value_toDouble(&value), 65.41); + value_free(&value); +} + +TEST_F(SensingBlocksTest, Of_YPositionOfSprite_CompileTime) +{ + auto targetMock = std::make_shared(); + targetMock->setEngine(&m_engineMock); + + Sprite targetSprite; + targetSprite.setX(65.41); + targetSprite.setY(-56.28); + + ScriptBuilder builder(m_extension.get(), m_engine, targetMock); + builder.addBlock("sensing_of"); + builder.addDropdownField("PROPERTY", "y position"); + builder.addDropdownInput("OBJECT", "Sprite2"); + Block *block = builder.currentBlock(); + + EXPECT_CALL(m_engineMock, findTarget("Sprite2")).WillOnce(Return(4)); + EXPECT_CALL(m_engineMock, targetAt(4)).WillOnce(Return(&targetSprite)); + Compiler compiler(&m_engineMock, targetMock.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(targetMock.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(targetMock.get(), &m_engineMock, &script); + + ValueData value = thread.runReporter(); + ASSERT_EQ(value_toDouble(&value), -56.28); + value_free(&value); +} + +TEST_F(SensingBlocksTest, Of_YPositionOfSprite_Runtime) +{ + auto targetMock = std::make_shared(); + targetMock->setEngine(&m_engineMock); + + Sprite targetSprite; + targetSprite.setX(65.41); + targetSprite.setY(-56.28); + + ScriptBuilder builder(m_extension.get(), m_engine, targetMock); + + builder.addBlock("test_const_string"); + builder.addValueInput("STRING", "Sprite2"); + auto valueBlock = builder.takeBlock(); + + builder.addBlock("sensing_of"); + builder.addDropdownField("PROPERTY", "y position"); + builder.addObscuredInput("OBJECT", valueBlock); + Block *block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, targetMock.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(targetMock.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(targetMock.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, findTarget("Sprite2")).WillOnce(Return(4)); + EXPECT_CALL(m_engineMock, targetAt(4)).WillOnce(Return(&targetSprite)); + ValueData value = thread.runReporter(); + ASSERT_EQ(value_toDouble(&value), -56.28); + value_free(&value); +} + +TEST_F(SensingBlocksTest, Of_DirectionOfSprite_CompileTime) +{ + auto targetMock = std::make_shared(); + targetMock->setEngine(&m_engineMock); + + Sprite targetSprite; + targetSprite.setDirection(73.8); + + ScriptBuilder builder(m_extension.get(), m_engine, targetMock); + builder.addBlock("sensing_of"); + builder.addDropdownField("PROPERTY", "direction"); + builder.addDropdownInput("OBJECT", "Sprite2"); + Block *block = builder.currentBlock(); + + EXPECT_CALL(m_engineMock, findTarget("Sprite2")).WillOnce(Return(4)); + EXPECT_CALL(m_engineMock, targetAt(4)).WillOnce(Return(&targetSprite)); + Compiler compiler(&m_engineMock, targetMock.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(targetMock.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(targetMock.get(), &m_engineMock, &script); + + ValueData value = thread.runReporter(); + ASSERT_EQ(value_toDouble(&value), 73.8); + value_free(&value); +} + +TEST_F(SensingBlocksTest, Of_DirectionOfSprite_Runtime) +{ + auto targetMock = std::make_shared(); + targetMock->setEngine(&m_engineMock); + + Sprite targetSprite; + targetSprite.setDirection(73.8); + + ScriptBuilder builder(m_extension.get(), m_engine, targetMock); + + builder.addBlock("test_const_string"); + builder.addValueInput("STRING", "Sprite2"); + auto valueBlock = builder.takeBlock(); + + builder.addBlock("sensing_of"); + builder.addDropdownField("PROPERTY", "direction"); + builder.addObscuredInput("OBJECT", valueBlock); + Block *block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, targetMock.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(targetMock.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(targetMock.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, findTarget("Sprite2")).WillOnce(Return(4)); + EXPECT_CALL(m_engineMock, targetAt(4)).WillOnce(Return(&targetSprite)); + ValueData value = thread.runReporter(); + ASSERT_EQ(value_toDouble(&value), 73.8); + value_free(&value); +} + +TEST_F(SensingBlocksTest, Of_CostumeNumberOfSprite_CompileTime) +{ + auto targetMock = std::make_shared(); + targetMock->setEngine(&m_engineMock); + + Sprite targetSprite; + targetSprite.addCostume(std::make_shared("costume1", "a", "png")); + targetSprite.addCostume(std::make_shared("costume2", "b", "svg")); + targetSprite.setCostumeIndex(1); + + ScriptBuilder builder(m_extension.get(), m_engine, targetMock); + builder.addBlock("sensing_of"); + builder.addDropdownField("PROPERTY", "costume #"); + builder.addDropdownInput("OBJECT", "Sprite2"); + Block *block = builder.currentBlock(); + + EXPECT_CALL(m_engineMock, findTarget("Sprite2")).WillOnce(Return(4)); + EXPECT_CALL(m_engineMock, targetAt(4)).WillOnce(Return(&targetSprite)); + Compiler compiler(&m_engineMock, targetMock.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(targetMock.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(targetMock.get(), &m_engineMock, &script); + + ValueData value = thread.runReporter(); + ASSERT_EQ(value_toDouble(&value), 2.0); + value_free(&value); +} + +TEST_F(SensingBlocksTest, Of_CostumeNumberOfSprite_Runtime) +{ + auto targetMock = std::make_shared(); + targetMock->setEngine(&m_engineMock); + + Sprite targetSprite; + targetSprite.addCostume(std::make_shared("costume1", "a", "png")); + targetSprite.addCostume(std::make_shared("costume2", "b", "svg")); + targetSprite.setCostumeIndex(1); + + ScriptBuilder builder(m_extension.get(), m_engine, targetMock); + + builder.addBlock("test_const_string"); + builder.addValueInput("STRING", "Sprite2"); + auto valueBlock = builder.takeBlock(); + + builder.addBlock("sensing_of"); + builder.addDropdownField("PROPERTY", "costume #"); + builder.addObscuredInput("OBJECT", valueBlock); + Block *block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, targetMock.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(targetMock.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(targetMock.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, findTarget("Sprite2")).WillOnce(Return(4)); + EXPECT_CALL(m_engineMock, targetAt(4)).WillOnce(Return(&targetSprite)); + ValueData value = thread.runReporter(); + ASSERT_EQ(value_toDouble(&value), 2.0); + value_free(&value); +} + +TEST_F(SensingBlocksTest, Of_CostumeNameOfSprite_CompileTime) +{ + auto targetMock = std::make_shared(); + targetMock->setEngine(&m_engineMock); + + Sprite targetSprite; + targetSprite.addCostume(std::make_shared("costume1", "a", "png")); + targetSprite.addCostume(std::make_shared("costume2", "b", "svg")); + targetSprite.setCostumeIndex(1); + + ScriptBuilder builder(m_extension.get(), m_engine, targetMock); + builder.addBlock("sensing_of"); + builder.addDropdownField("PROPERTY", "costume name"); + builder.addDropdownInput("OBJECT", "Sprite2"); + Block *block = builder.currentBlock(); + + EXPECT_CALL(m_engineMock, findTarget("Sprite2")).WillOnce(Return(4)); + EXPECT_CALL(m_engineMock, targetAt(4)).WillOnce(Return(&targetSprite)); + Compiler compiler(&m_engineMock, targetMock.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(targetMock.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(targetMock.get(), &m_engineMock, &script); + + ValueData value = thread.runReporter(); + std::string str; + value_toString(&value, &str); + value_free(&value); + ASSERT_EQ(str, "costume2"); +} + +TEST_F(SensingBlocksTest, Of_CostumeNameOfSprite_Runtime) +{ + auto targetMock = std::make_shared(); + targetMock->setEngine(&m_engineMock); + + Sprite targetSprite; + targetSprite.addCostume(std::make_shared("costume1", "a", "png")); + targetSprite.addCostume(std::make_shared("costume2", "b", "svg")); + targetSprite.setCostumeIndex(1); + + ScriptBuilder builder(m_extension.get(), m_engine, targetMock); + + builder.addBlock("test_const_string"); + builder.addValueInput("STRING", "Sprite2"); + auto valueBlock = builder.takeBlock(); + + builder.addBlock("sensing_of"); + builder.addDropdownField("PROPERTY", "costume name"); + builder.addObscuredInput("OBJECT", valueBlock); + Block *block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, targetMock.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(targetMock.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(targetMock.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, findTarget("Sprite2")).WillOnce(Return(4)); + EXPECT_CALL(m_engineMock, targetAt(4)).WillOnce(Return(&targetSprite)); + ValueData value = thread.runReporter(); + std::string str; + value_toString(&value, &str); + value_free(&value); + ASSERT_EQ(str, "costume2"); +} + +TEST_F(SensingBlocksTest, Of_SizeOfSprite_CompileTime) +{ + auto targetMock = std::make_shared(); + targetMock->setEngine(&m_engineMock); + + Sprite targetSprite; + targetSprite.setSize(47.32); + + ScriptBuilder builder(m_extension.get(), m_engine, targetMock); + builder.addBlock("sensing_of"); + builder.addDropdownField("PROPERTY", "size"); + builder.addDropdownInput("OBJECT", "Sprite2"); + Block *block = builder.currentBlock(); + + EXPECT_CALL(m_engineMock, findTarget("Sprite2")).WillOnce(Return(4)); + EXPECT_CALL(m_engineMock, targetAt(4)).WillOnce(Return(&targetSprite)); + Compiler compiler(&m_engineMock, targetMock.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(targetMock.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(targetMock.get(), &m_engineMock, &script); + + ValueData value = thread.runReporter(); + ASSERT_EQ(value_toDouble(&value), 47.32); + value_free(&value); +} + +TEST_F(SensingBlocksTest, Of_SizeOfSprite_Runtime) +{ + auto targetMock = std::make_shared(); + targetMock->setEngine(&m_engineMock); + + Sprite targetSprite; + targetSprite.setSize(47.32); + + ScriptBuilder builder(m_extension.get(), m_engine, targetMock); + + builder.addBlock("test_const_string"); + builder.addValueInput("STRING", "Sprite2"); + auto valueBlock = builder.takeBlock(); + + builder.addBlock("sensing_of"); + builder.addDropdownField("PROPERTY", "size"); + builder.addObscuredInput("OBJECT", valueBlock); + Block *block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, targetMock.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(targetMock.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(targetMock.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, findTarget("Sprite2")).WillOnce(Return(4)); + EXPECT_CALL(m_engineMock, targetAt(4)).WillOnce(Return(&targetSprite)); + ValueData value = thread.runReporter(); + ASSERT_EQ(value_toDouble(&value), 47.32); + value_free(&value); +} + +TEST_F(SensingBlocksTest, Of_VolumeOfTarget_CompileTime) +{ + auto targetMock = std::make_shared(); + targetMock->setEngine(&m_engineMock); + + TargetMock target; + target.setVolume(89.46); + + ScriptBuilder builder(m_extension.get(), m_engine, targetMock); + builder.addBlock("sensing_of"); + builder.addDropdownField("PROPERTY", "volume"); + builder.addDropdownInput("OBJECT", "test"); + Block *block = builder.currentBlock(); + + EXPECT_CALL(m_engineMock, findTarget("test")).WillOnce(Return(4)); + EXPECT_CALL(m_engineMock, targetAt(4)).WillOnce(Return(&target)); + Compiler compiler(&m_engineMock, targetMock.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(targetMock.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(targetMock.get(), &m_engineMock, &script); + + ValueData value = thread.runReporter(); + ASSERT_EQ(value_toDouble(&value), 89.46); + value_free(&value); +} + +TEST_F(SensingBlocksTest, Of_VolumeOfTarget_Runtime) +{ + auto targetMock = std::make_shared(); + targetMock->setEngine(&m_engineMock); + + TargetMock target; + target.setVolume(89.46); + + ScriptBuilder builder(m_extension.get(), m_engine, targetMock); + + builder.addBlock("test_const_string"); + builder.addValueInput("STRING", "test"); + auto valueBlock = builder.takeBlock(); + + builder.addBlock("sensing_of"); + builder.addDropdownField("PROPERTY", "volume"); + builder.addObscuredInput("OBJECT", valueBlock); + Block *block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, targetMock.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(targetMock.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(targetMock.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, findTarget("test")).WillOnce(Return(4)); + EXPECT_CALL(m_engineMock, targetAt(4)).WillOnce(Return(&target)); + ValueData value = thread.runReporter(); + ASSERT_EQ(value_toDouble(&value), 89.46); + value_free(&value); +} + +TEST_F(SensingBlocksTest, Of_VariableOfTarget_CompileTime) +{ + auto targetMock = std::make_shared(); + targetMock->setEngine(&m_engineMock); + + TargetMock target; + auto v1 = std::make_shared("v1", "var1", 64.13); + auto v2 = std::make_shared("v2", "var2", 98); + auto v3 = std::make_shared("v3", "var3", -0.85); + target.addVariable(v1); + target.addVariable(v2); + target.addVariable(v3); + + ScriptBuilder builder(m_extension.get(), m_engine, targetMock); + builder.addBlock("sensing_of"); + builder.addDropdownField("PROPERTY", "var2"); + builder.addDropdownInput("OBJECT", "test"); + Block *block = builder.currentBlock(); + + EXPECT_CALL(m_engineMock, findTarget("test")).WillOnce(Return(4)); + EXPECT_CALL(m_engineMock, targetAt(4)).WillOnce(Return(&target)); + Compiler compiler(&m_engineMock, targetMock.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(targetMock.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(targetMock.get(), &m_engineMock, &script); + + ValueData value = thread.runReporter(); + ASSERT_EQ(value_toDouble(&value), 98.0); + value_free(&value); +} + +TEST_F(SensingBlocksTest, Of_VariableOfTarget_Runtime) +{ + auto targetMock = std::make_shared(); + targetMock->setEngine(&m_engineMock); + + TargetMock target; + auto v1 = std::make_shared("v1", "var1", 64.13); + auto v2 = std::make_shared("v2", "var2", 98); + auto v3 = std::make_shared("v3", "var3", -0.85); + target.addVariable(v1); + target.addVariable(v2); + target.addVariable(v3); + + ScriptBuilder builder(m_extension.get(), m_engine, targetMock); + + builder.addBlock("test_const_string"); + builder.addValueInput("STRING", "test"); + auto valueBlock = builder.takeBlock(); + + builder.addBlock("sensing_of"); + builder.addDropdownField("PROPERTY", "var2"); + builder.addObscuredInput("OBJECT", valueBlock); + Block *block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, targetMock.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(targetMock.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(targetMock.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, findTarget("test")).WillOnce(Return(4)); + EXPECT_CALL(m_engineMock, targetAt(4)).WillOnce(Return(&target)); + ValueData value = thread.runReporter(); + ASSERT_EQ(value_toDouble(&value), 98.0); + value_free(&value); +} + +TEST_F(SensingBlocksTest, Of_BackdropNumberOfStage_CompileTime) +{ + auto targetMock = std::make_shared(); + targetMock->setEngine(&m_engineMock); + + Stage stage; + stage.addCostume(std::make_shared("backdrop1", "a", "png")); + stage.addCostume(std::make_shared("backdrop2", "b", "svg")); + stage.addCostume(std::make_shared("backdrop3", "b", "svg")); + stage.setCostumeIndex(2); + + ScriptBuilder builder(m_extension.get(), m_engine, targetMock); + builder.addBlock("sensing_of"); + builder.addDropdownField("PROPERTY", "backdrop #"); + builder.addDropdownInput("OBJECT", "_stage_"); + Block *block = builder.currentBlock(); + + EXPECT_CALL(m_engineMock, findTarget("_stage_")).WillRepeatedly(Return(3)); + EXPECT_CALL(m_engineMock, targetAt(3)).WillRepeatedly(Return(&stage)); + EXPECT_CALL(m_engineMock, stage()).WillRepeatedly(Return(&stage)); + Compiler compiler(&m_engineMock, targetMock.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(targetMock.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(targetMock.get(), &m_engineMock, &script); + + ValueData value = thread.runReporter(); + ASSERT_EQ(value_toDouble(&value), 3.0); + value_free(&value); +} + +TEST_F(SensingBlocksTest, Of_BackdropNumberOfStage_Runtime) +{ + auto targetMock = std::make_shared(); + targetMock->setEngine(&m_engineMock); + + Stage stage; + stage.addCostume(std::make_shared("backdrop1", "a", "png")); + stage.addCostume(std::make_shared("backdrop2", "b", "svg")); + stage.addCostume(std::make_shared("backdrop3", "b", "svg")); + stage.setCostumeIndex(2); + + ScriptBuilder builder(m_extension.get(), m_engine, targetMock); + + builder.addBlock("test_const_string"); + builder.addValueInput("STRING", "_stage_"); + auto valueBlock = builder.takeBlock(); + + builder.addBlock("sensing_of"); + builder.addDropdownField("PROPERTY", "backdrop #"); + builder.addObscuredInput("OBJECT", valueBlock); + Block *block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, targetMock.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(targetMock.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(targetMock.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, findTarget("_stage_")).WillRepeatedly(Return(3)); + EXPECT_CALL(m_engineMock, targetAt(3)).WillRepeatedly(Return(&stage)); + EXPECT_CALL(m_engineMock, stage()).WillRepeatedly(Return(&stage)); + ValueData value = thread.runReporter(); + ASSERT_EQ(value_toDouble(&value), 3.0); + value_free(&value); +} + +TEST_F(SensingBlocksTest, Of_BackdropNameOfStage_CompileTime) +{ + auto targetMock = std::make_shared(); + targetMock->setEngine(&m_engineMock); + + Stage stage; + stage.addCostume(std::make_shared("backdrop1", "a", "png")); + stage.addCostume(std::make_shared("backdrop2", "b", "svg")); + stage.addCostume(std::make_shared("backdrop3", "b", "svg")); + stage.setCostumeIndex(2); + + ScriptBuilder builder(m_extension.get(), m_engine, targetMock); + builder.addBlock("sensing_of"); + builder.addDropdownField("PROPERTY", "backdrop name"); + builder.addDropdownInput("OBJECT", "_stage_"); + Block *block = builder.currentBlock(); + + EXPECT_CALL(m_engineMock, findTarget("_stage_")).WillRepeatedly(Return(3)); + EXPECT_CALL(m_engineMock, targetAt(3)).WillRepeatedly(Return(&stage)); + EXPECT_CALL(m_engineMock, stage()).WillRepeatedly(Return(&stage)); + Compiler compiler(&m_engineMock, targetMock.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(targetMock.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(targetMock.get(), &m_engineMock, &script); + + ValueData value = thread.runReporter(); + std::string str; + value_toString(&value, &str); + value_free(&value); + ASSERT_EQ(str, "backdrop3"); +} + +TEST_F(SensingBlocksTest, Of_BackdropNameOfStage_Runtime) +{ + auto targetMock = std::make_shared(); + targetMock->setEngine(&m_engineMock); + + Stage stage; + stage.addCostume(std::make_shared("backdrop1", "a", "png")); + stage.addCostume(std::make_shared("backdrop2", "b", "svg")); + stage.addCostume(std::make_shared("backdrop3", "b", "svg")); + stage.setCostumeIndex(2); + + ScriptBuilder builder(m_extension.get(), m_engine, targetMock); + + builder.addBlock("test_const_string"); + builder.addValueInput("STRING", "_stage_"); + auto valueBlock = builder.takeBlock(); + + builder.addBlock("sensing_of"); + builder.addDropdownField("PROPERTY", "backdrop name"); + builder.addObscuredInput("OBJECT", valueBlock); + Block *block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, targetMock.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(targetMock.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(targetMock.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, findTarget("_stage_")).WillRepeatedly(Return(3)); + EXPECT_CALL(m_engineMock, targetAt(3)).WillRepeatedly(Return(&stage)); + EXPECT_CALL(m_engineMock, stage()).WillRepeatedly(Return(&stage)); + ValueData value = thread.runReporter(); + std::string str; + value_toString(&value, &str); + value_free(&value); + ASSERT_EQ(str, "backdrop3"); +} + +TEST_F(SensingBlocksTest, Of_InvalidTarget_CompileTime) +{ + auto targetMock = std::make_shared(); + targetMock->setEngine(&m_engineMock); + + ScriptBuilder builder(m_extension.get(), m_engine, targetMock); + builder.addBlock("sensing_of"); + builder.addDropdownField("PROPERTY", "volume"); + builder.addDropdownInput("OBJECT", "test"); + Block *block = builder.currentBlock(); + + EXPECT_CALL(m_engineMock, findTarget("test")).WillOnce(Return(-1)); + EXPECT_CALL(m_engineMock, targetAt(-1)).WillRepeatedly(Return(nullptr)); + Compiler compiler(&m_engineMock, targetMock.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(targetMock.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(targetMock.get(), &m_engineMock, &script); + + ValueData value = thread.runReporter(); + ASSERT_EQ(value_toDouble(&value), 0.0); + value_free(&value); +} + +TEST_F(SensingBlocksTest, Of_InvalidTarget_Runtime) +{ + auto targetMock = std::make_shared(); + targetMock->setEngine(&m_engineMock); + + ScriptBuilder builder(m_extension.get(), m_engine, targetMock); + + builder.addBlock("test_const_string"); + builder.addValueInput("STRING", "test"); + auto valueBlock = builder.takeBlock(); + + builder.addBlock("sensing_of"); + builder.addDropdownField("PROPERTY", "volume"); + builder.addObscuredInput("OBJECT", valueBlock); + Block *block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, targetMock.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(targetMock.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(targetMock.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, findTarget("test")).WillOnce(Return(-1)); + EXPECT_CALL(m_engineMock, targetAt(-1)).WillRepeatedly(Return(nullptr)); + ValueData value = thread.runReporter(); + ASSERT_EQ(value_toDouble(&value), 0.0); + value_free(&value); +} + +TEST_F(SensingBlocksTest, Of_InvalidProperty_Sprite_CompileTime) +{ + auto targetMock = std::make_shared(); + targetMock->setEngine(&m_engineMock); + + Sprite targetSprite; + + ScriptBuilder builder(m_extension.get(), m_engine, targetMock); + builder.addBlock("sensing_of"); + builder.addDropdownField("PROPERTY", "test"); + builder.addDropdownInput("OBJECT", "Sprite2"); + Block *block = builder.currentBlock(); + + EXPECT_CALL(m_engineMock, findTarget("Sprite2")).WillRepeatedly(Return(4)); + EXPECT_CALL(m_engineMock, targetAt(4)).WillRepeatedly(Return(&targetSprite)); + Compiler compiler(&m_engineMock, targetMock.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(targetMock.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(targetMock.get(), &m_engineMock, &script); + + ValueData value = thread.runReporter(); + ASSERT_EQ(value_toDouble(&value), 0.0); + value_free(&value); +} + +TEST_F(SensingBlocksTest, Of_InvalidProperty_Sprite_Runtime) +{ + auto targetMock = std::make_shared(); + targetMock->setEngine(&m_engineMock); + + Sprite targetSprite; + + ScriptBuilder builder(m_extension.get(), m_engine, targetMock); + + builder.addBlock("test_const_string"); + builder.addValueInput("STRING", "Sprite2"); + auto valueBlock = builder.takeBlock(); + + builder.addBlock("sensing_of"); + builder.addDropdownField("PROPERTY", "test"); + builder.addObscuredInput("OBJECT", valueBlock); + Block *block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, targetMock.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(targetMock.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(targetMock.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, findTarget("Sprite2")).WillRepeatedly(Return(4)); + EXPECT_CALL(m_engineMock, targetAt(4)).WillRepeatedly(Return(&targetSprite)); + ValueData value = thread.runReporter(); + ASSERT_EQ(value_toDouble(&value), 0.0); + value_free(&value); +} + +TEST_F(SensingBlocksTest, Of_InvalidProperty_Stage_CompileTime) +{ + auto targetMock = std::make_shared(); + targetMock->setEngine(&m_engineMock); + + Stage stage; + + ScriptBuilder builder(m_extension.get(), m_engine, targetMock); + builder.addBlock("sensing_of"); + builder.addDropdownField("PROPERTY", "test"); + builder.addDropdownInput("OBJECT", "Sprite2"); + Block *block = builder.currentBlock(); + + EXPECT_CALL(m_engineMock, findTarget("Sprite2")).WillRepeatedly(Return(4)); + EXPECT_CALL(m_engineMock, targetAt(4)).WillRepeatedly(Return(&stage)); + Compiler compiler(&m_engineMock, targetMock.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(targetMock.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(targetMock.get(), &m_engineMock, &script); + + ValueData value = thread.runReporter(); + ASSERT_EQ(value_toDouble(&value), 0.0); + value_free(&value); +} + +TEST_F(SensingBlocksTest, Of_InvalidProperty_Stage_Runtime) +{ + auto targetMock = std::make_shared(); + targetMock->setEngine(&m_engineMock); + + Stage stage; + + ScriptBuilder builder(m_extension.get(), m_engine, targetMock); + + builder.addBlock("test_const_string"); + builder.addValueInput("STRING", "Sprite2"); + auto valueBlock = builder.takeBlock(); + + builder.addBlock("sensing_of"); + builder.addDropdownField("PROPERTY", "test"); + builder.addObscuredInput("OBJECT", valueBlock); + Block *block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, targetMock.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(targetMock.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(targetMock.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, findTarget("Sprite2")).WillRepeatedly(Return(4)); + EXPECT_CALL(m_engineMock, targetAt(4)).WillRepeatedly(Return(&stage)); + ValueData value = thread.runReporter(); + ASSERT_EQ(value_toDouble(&value), 0.0); + value_free(&value); +} + +TEST_F(SensingBlocksTest, Of_Invalid_XPositionOfStage_CompileTime) +{ + auto targetMock = std::make_shared(); + targetMock->setEngine(&m_engineMock); + + Stage stage; + + ScriptBuilder builder(m_extension.get(), m_engine, targetMock); + builder.addBlock("sensing_of"); + builder.addDropdownField("PROPERTY", "x position"); + builder.addDropdownInput("OBJECT", "_stage_"); + Block *block = builder.currentBlock(); + + EXPECT_CALL(m_engineMock, findTarget("_stage_")).WillRepeatedly(Return(3)); + EXPECT_CALL(m_engineMock, targetAt(3)).WillRepeatedly(Return(&stage)); + EXPECT_CALL(m_engineMock, stage()).WillRepeatedly(Return(&stage)); + Compiler compiler(&m_engineMock, targetMock.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(targetMock.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(targetMock.get(), &m_engineMock, &script); + + ValueData value = thread.runReporter(); + ASSERT_EQ(value_toDouble(&value), 0.0); + value_free(&value); +} + +TEST_F(SensingBlocksTest, Of_Invalid_DirectionOfStage_Runtime) +{ + auto targetMock = std::make_shared(); + targetMock->setEngine(&m_engineMock); + + Stage stage; + + ScriptBuilder builder(m_extension.get(), m_engine, targetMock); + + builder.addBlock("test_const_string"); + builder.addValueInput("STRING", "_stage_"); + auto valueBlock = builder.takeBlock(); + + builder.addBlock("sensing_of"); + builder.addDropdownField("PROPERTY", "direction"); + builder.addObscuredInput("OBJECT", valueBlock); + Block *block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, targetMock.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(targetMock.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(targetMock.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, findTarget("_stage_")).WillRepeatedly(Return(3)); + EXPECT_CALL(m_engineMock, targetAt(3)).WillRepeatedly(Return(&stage)); + EXPECT_CALL(m_engineMock, stage()).WillRepeatedly(Return(&stage)); + ValueData value = thread.runReporter(); + ASSERT_EQ(value_toDouble(&value), 0.0); + value_free(&value); +} + +TEST_F(SensingBlocksTest, Of_Invalid_CostumeNumberOfStage_CompileTime) +{ + auto targetMock = std::make_shared(); + targetMock->setEngine(&m_engineMock); + + Stage stage; + stage.addCostume(std::make_shared("backdrop1", "a", "png")); + stage.addCostume(std::make_shared("backdrop2", "b", "svg")); + stage.addCostume(std::make_shared("backdrop3", "b", "svg")); + stage.setCostumeIndex(2); + + ScriptBuilder builder(m_extension.get(), m_engine, targetMock); + builder.addBlock("sensing_of"); + builder.addDropdownField("PROPERTY", "costume #"); + builder.addDropdownInput("OBJECT", "_stage_"); + Block *block = builder.currentBlock(); + + EXPECT_CALL(m_engineMock, findTarget("_stage_")).WillRepeatedly(Return(3)); + EXPECT_CALL(m_engineMock, targetAt(3)).WillRepeatedly(Return(&stage)); + EXPECT_CALL(m_engineMock, stage()).WillRepeatedly(Return(&stage)); + Compiler compiler(&m_engineMock, targetMock.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(targetMock.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(targetMock.get(), &m_engineMock, &script); + + ValueData value = thread.runReporter(); + ASSERT_EQ(value_toDouble(&value), 0.0); + value_free(&value); +} + +TEST_F(SensingBlocksTest, Of_Invalid_CostumeNameOfStage_Runtime) +{ + auto targetMock = std::make_shared(); + targetMock->setEngine(&m_engineMock); + + Stage stage; + stage.addCostume(std::make_shared("backdrop1", "a", "png")); + stage.addCostume(std::make_shared("backdrop2", "b", "svg")); + stage.addCostume(std::make_shared("backdrop3", "b", "svg")); + stage.setCostumeIndex(2); + + ScriptBuilder builder(m_extension.get(), m_engine, targetMock); + + builder.addBlock("test_const_string"); + builder.addValueInput("STRING", "_stage_"); + auto valueBlock = builder.takeBlock(); + + builder.addBlock("sensing_of"); + builder.addDropdownField("PROPERTY", "costume name"); + builder.addObscuredInput("OBJECT", valueBlock); + Block *block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, targetMock.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(targetMock.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(targetMock.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, findTarget("_stage_")).WillRepeatedly(Return(3)); + EXPECT_CALL(m_engineMock, targetAt(3)).WillRepeatedly(Return(&stage)); + EXPECT_CALL(m_engineMock, stage()).WillRepeatedly(Return(&stage)); + ValueData value = thread.runReporter(); + std::string str; + value_toString(&value, &str); + value_free(&value); + ASSERT_EQ(str, "0"); +} + +TEST_F(SensingBlocksTest, Of_Invalid_BackdropNameOfSprite_CompileTime) +{ + auto targetMock = std::make_shared(); + targetMock->setEngine(&m_engineMock); + + Sprite targetSprite; + targetSprite.addCostume(std::make_shared("costume1", "a", "png")); + targetSprite.addCostume(std::make_shared("costume2", "b", "svg")); + targetSprite.setCostumeIndex(1); + + ScriptBuilder builder(m_extension.get(), m_engine, targetMock); + builder.addBlock("sensing_of"); + builder.addDropdownField("PROPERTY", "backdrop name"); + builder.addDropdownInput("OBJECT", "Sprite2"); + Block *block = builder.currentBlock(); + + EXPECT_CALL(m_engineMock, findTarget("Sprite2")).WillOnce(Return(4)); + EXPECT_CALL(m_engineMock, targetAt(4)).WillOnce(Return(&targetSprite)); + Compiler compiler(&m_engineMock, targetMock.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(targetMock.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(targetMock.get(), &m_engineMock, &script); + + ValueData value = thread.runReporter(); + std::string str; + value_toString(&value, &str); + value_free(&value); + ASSERT_EQ(str, "0"); +} + +TEST_F(SensingBlocksTest, Of_Invalid_BackdropNumberOfSprite_Runtime) +{ + auto targetMock = std::make_shared(); + targetMock->setEngine(&m_engineMock); + + Sprite targetSprite; + targetSprite.addCostume(std::make_shared("costume1", "a", "png")); + targetSprite.addCostume(std::make_shared("costume2", "b", "svg")); + targetSprite.setCostumeIndex(1); + + ScriptBuilder builder(m_extension.get(), m_engine, targetMock); + + builder.addBlock("test_const_string"); + builder.addValueInput("STRING", "Sprite2"); + auto valueBlock = builder.takeBlock(); + + builder.addBlock("sensing_of"); + builder.addDropdownField("PROPERTY", "backdrop #"); + builder.addObscuredInput("OBJECT", valueBlock); + Block *block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, targetMock.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(targetMock.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(targetMock.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, findTarget("Sprite2")).WillOnce(Return(4)); + EXPECT_CALL(m_engineMock, targetAt(4)).WillOnce(Return(&targetSprite)); + ValueData value = thread.runReporter(); + ASSERT_EQ(value_toDouble(&value), 0.0); + value_free(&value); +} + +TEST_F(SensingBlocksTest, Of_PreferPropertiesOverVariables_CompileTime) +{ + auto targetMock = std::make_shared(); + targetMock->setEngine(&m_engineMock); + + Sprite sprite; + auto v1 = std::make_shared("v1", "var1", 64.13); + auto v2 = std::make_shared("v2", "var2", 98); + auto v3 = std::make_shared("v3", "x position", -0.85); + sprite.addVariable(v1); + sprite.addVariable(v2); + sprite.addVariable(v3); + + sprite.setX(-78.25); + + ScriptBuilder builder(m_extension.get(), m_engine, targetMock); + builder.addBlock("sensing_of"); + builder.addDropdownField("PROPERTY", "x position"); + builder.addDropdownInput("OBJECT", "test"); + Block *block = builder.currentBlock(); + + EXPECT_CALL(m_engineMock, findTarget("test")).WillOnce(Return(7)); + EXPECT_CALL(m_engineMock, targetAt(7)).WillOnce(Return(&sprite)); + Compiler compiler(&m_engineMock, targetMock.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(targetMock.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(targetMock.get(), &m_engineMock, &script); + + ValueData value = thread.runReporter(); + ASSERT_EQ(value_toDouble(&value), -78.25); + value_free(&value); +} + +TEST_F(SensingBlocksTest, Of_PreferPropertiesOverVariables_Runtime) +{ + auto targetMock = std::make_shared(); + targetMock->setEngine(&m_engineMock); + + Sprite sprite; + auto v1 = std::make_shared("v1", "var1", 64.13); + auto v2 = std::make_shared("v2", "var2", 98); + auto v3 = std::make_shared("v3", "x position", -0.85); + sprite.addVariable(v1); + sprite.addVariable(v2); + sprite.addVariable(v3); + + sprite.setX(-78.25); + + ScriptBuilder builder(m_extension.get(), m_engine, targetMock); + + builder.addBlock("test_const_string"); + builder.addValueInput("STRING", "test"); + auto valueBlock = builder.takeBlock(); + + builder.addBlock("sensing_of"); + builder.addDropdownField("PROPERTY", "x position"); + builder.addObscuredInput("OBJECT", valueBlock); + Block *block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, targetMock.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(targetMock.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(targetMock.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, findTarget("test")).WillOnce(Return(4)); + EXPECT_CALL(m_engineMock, targetAt(4)).WillOnce(Return(&sprite)); + ValueData value = thread.runReporter(); + ASSERT_EQ(value_toDouble(&value), -78.25); + value_free(&value); +} From 05b0fbd077d49cf6864057d14e453c22f5413ee1 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sun, 10 Aug 2025 17:44:56 +0200 Subject: [PATCH 19/20] Implement sensing_current block --- src/blocks/sensingblocks.cpp | 74 ++++++++++++ src/blocks/sensingblocks.h | 1 + test/blocks/sensing_blocks_test.cpp | 174 ++++++++++++++++++++++++++++ 3 files changed, 249 insertions(+) diff --git a/src/blocks/sensingblocks.cpp b/src/blocks/sensingblocks.cpp index 91ce34188..2bd2b8564 100644 --- a/src/blocks/sensingblocks.cpp +++ b/src/blocks/sensingblocks.cpp @@ -59,6 +59,7 @@ void SensingBlocks::registerBlocks(IEngine *engine) engine->addCompileFunction(this, "sensing_timer", &compileTimer); engine->addCompileFunction(this, "sensing_resettimer", &compileResetTimer); engine->addCompileFunction(this, "sensing_of", &compileOf); + engine->addCompileFunction(this, "sensing_current", &compileCurrent); } void SensingBlocks::onInit(IEngine *engine) @@ -342,6 +343,30 @@ CompilerValue *SensingBlocks::compileOf(Compiler *compiler) return compiler->addConstValue(0.0); } +CompilerValue *SensingBlocks::compileCurrent(Compiler *compiler) +{ + Field *field = compiler->field("CURRENTMENU"); + assert(field); + std::string option = field->value().toString(); + + if (option == "YEAR") + return compiler->addFunctionCall("sensing_current_year", Compiler::StaticType::Number); + else if (option == "MONTH") + return compiler->addFunctionCall("sensing_current_month", Compiler::StaticType::Number); + else if (option == "DATE") + return compiler->addFunctionCall("sensing_current_date", Compiler::StaticType::Number); + else if (option == "DAYOFWEEK") + return compiler->addFunctionCall("sensing_current_day_of_week", Compiler::StaticType::Number); + else if (option == "HOUR") + return compiler->addFunctionCall("sensing_current_hour", Compiler::StaticType::Number); + else if (option == "MINUTE") + return compiler->addFunctionCall("sensing_current_minute", Compiler::StaticType::Number); + else if (option == "SECOND") + return compiler->addFunctionCall("sensing_current_second", Compiler::StaticType::Number); + else + return compiler->addConstValue(Value()); +} + void SensingBlocks::onAnswer(const std::string &answer) { // https://github.com/scratchfoundation/scratch-vm/blob/6055823f203a696165084b873e661713806583ec/src/blocks/scratch3_sensing.js#L99-L115 @@ -701,3 +726,52 @@ extern "C" ValueData sensing_variable_of_target(Target *target, const StringPtr value_init(&ret); return ret; } + +extern "C" double sensing_current_year() +{ + time_t now = time(0); + tm *ltm = localtime(&now); + return ltm->tm_year + 1900; +} + +extern "C" double sensing_current_month() +{ + time_t now = time(0); + tm *ltm = localtime(&now); + return ltm->tm_mon + 1; +} + +extern "C" double sensing_current_date() +{ + time_t now = time(0); + tm *ltm = localtime(&now); + return ltm->tm_mday; +} + +extern "C" double sensing_current_day_of_week() +{ + time_t now = time(0); + tm *ltm = localtime(&now); + return ltm->tm_wday + 1; +} + +extern "C" double sensing_current_hour() +{ + time_t now = time(0); + tm *ltm = localtime(&now); + return ltm->tm_hour; +} + +extern "C" double sensing_current_minute() +{ + time_t now = time(0); + tm *ltm = localtime(&now); + return ltm->tm_min; +} + +extern "C" double sensing_current_second() +{ + time_t now = time(0); + tm *ltm = localtime(&now); + return ltm->tm_sec; +} diff --git a/src/blocks/sensingblocks.h b/src/blocks/sensingblocks.h index 2ba254898..e1c10e032 100644 --- a/src/blocks/sensingblocks.h +++ b/src/blocks/sensingblocks.h @@ -61,6 +61,7 @@ class SensingBlocks : public IExtension static CompilerValue *compileTimer(Compiler *compiler); static CompilerValue *compileResetTimer(Compiler *compiler); static CompilerValue *compileOf(Compiler *compiler); + static CompilerValue *compileCurrent(Compiler *compiler); static void onAnswer(const std::string &answer); static void enqueueAsk(const std::string &question, Thread *thread); diff --git a/test/blocks/sensing_blocks_test.cpp b/test/blocks/sensing_blocks_test.cpp index d610ebbaa..f0c25130b 100644 --- a/test/blocks/sensing_blocks_test.cpp +++ b/test/blocks/sensing_blocks_test.cpp @@ -2571,3 +2571,177 @@ TEST_F(SensingBlocksTest, Of_PreferPropertiesOverVariables_Runtime) ASSERT_EQ(value_toDouble(&value), -78.25); value_free(&value); } + +TEST_F(SensingBlocksTest, Current_Year) +{ + auto targetMock = std::make_shared(); + + ScriptBuilder builder(m_extension.get(), m_engine, targetMock); + builder.addBlock("sensing_current"); + builder.addDropdownField("CURRENTMENU", "YEAR"); + Block *block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, targetMock.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(targetMock.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(targetMock.get(), &m_engineMock, &script); + + ValueData value = thread.runReporter(); + time_t now = time(0); + tm *ltm = localtime(&now); + ASSERT_EQ(value_toDouble(&value), ltm->tm_year + 1900); + value_free(&value); +} + +TEST_F(SensingBlocksTest, Current_Month) +{ + auto targetMock = std::make_shared(); + + ScriptBuilder builder(m_extension.get(), m_engine, targetMock); + builder.addBlock("sensing_current"); + builder.addDropdownField("CURRENTMENU", "MONTH"); + Block *block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, targetMock.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(targetMock.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(targetMock.get(), &m_engineMock, &script); + + ValueData value = thread.runReporter(); + time_t now = time(0); + tm *ltm = localtime(&now); + ASSERT_EQ(value_toDouble(&value), ltm->tm_mon + 1); + value_free(&value); +} + +TEST_F(SensingBlocksTest, Current_Date) +{ + auto targetMock = std::make_shared(); + + ScriptBuilder builder(m_extension.get(), m_engine, targetMock); + builder.addBlock("sensing_current"); + builder.addDropdownField("CURRENTMENU", "DATE"); + Block *block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, targetMock.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(targetMock.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(targetMock.get(), &m_engineMock, &script); + + ValueData value = thread.runReporter(); + time_t now = time(0); + tm *ltm = localtime(&now); + ASSERT_EQ(value_toDouble(&value), ltm->tm_mday); + value_free(&value); +} + +TEST_F(SensingBlocksTest, Current_DayOfWeek) +{ + auto targetMock = std::make_shared(); + + ScriptBuilder builder(m_extension.get(), m_engine, targetMock); + builder.addBlock("sensing_current"); + builder.addDropdownField("CURRENTMENU", "DAYOFWEEK"); + Block *block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, targetMock.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(targetMock.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(targetMock.get(), &m_engineMock, &script); + + ValueData value = thread.runReporter(); + time_t now = time(0); + tm *ltm = localtime(&now); + ASSERT_EQ(value_toDouble(&value), ltm->tm_wday + 1); + value_free(&value); +} + +TEST_F(SensingBlocksTest, Current_Hour) +{ + auto targetMock = std::make_shared(); + + ScriptBuilder builder(m_extension.get(), m_engine, targetMock); + builder.addBlock("sensing_current"); + builder.addDropdownField("CURRENTMENU", "HOUR"); + Block *block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, targetMock.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(targetMock.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(targetMock.get(), &m_engineMock, &script); + + ValueData value = thread.runReporter(); + time_t now = time(0); + tm *ltm = localtime(&now); + ASSERT_EQ(value_toDouble(&value), ltm->tm_hour); + value_free(&value); +} + +TEST_F(SensingBlocksTest, Current_Minute) +{ + auto targetMock = std::make_shared(); + + ScriptBuilder builder(m_extension.get(), m_engine, targetMock); + builder.addBlock("sensing_current"); + builder.addDropdownField("CURRENTMENU", "MINUTE"); + Block *block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, targetMock.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(targetMock.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(targetMock.get(), &m_engineMock, &script); + + ValueData value = thread.runReporter(); + time_t now = time(0); + tm *ltm = localtime(&now); + ASSERT_EQ(value_toDouble(&value), ltm->tm_min); + value_free(&value); +} + +TEST_F(SensingBlocksTest, Current_Second) +{ + auto targetMock = std::make_shared(); + + ScriptBuilder builder(m_extension.get(), m_engine, targetMock); + builder.addBlock("sensing_current"); + builder.addDropdownField("CURRENTMENU", "SECOND"); + Block *block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, targetMock.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(targetMock.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(targetMock.get(), &m_engineMock, &script); + + ValueData value = thread.runReporter(); + time_t now = time(0); + tm *ltm = localtime(&now); + ASSERT_EQ(value_toDouble(&value), ltm->tm_sec); + value_free(&value); +} + +TEST_F(SensingBlocksTest, Current_Invalid) +{ + auto targetMock = std::make_shared(); + + ScriptBuilder builder(m_extension.get(), m_engine, targetMock); + builder.addBlock("sensing_current"); + builder.addDropdownField("CURRENTMENU", "TEST"); + Block *block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, targetMock.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(targetMock.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(targetMock.get(), &m_engineMock, &script); + + ValueData value = thread.runReporter(); + ASSERT_EQ(value_toDouble(&value), 0.0); + value_free(&value); +} From 5781a8a989839aafa94aa35fa0118285cd801d29 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sun, 10 Aug 2025 17:58:57 +0200 Subject: [PATCH 20/20] Implement sensing_dayssince2000 block --- src/blocks/sensingblocks.cpp | 16 ++++++++++++++++ src/blocks/sensingblocks.h | 3 +++ test/blocks/sensing_blocks_test.cpp | 27 +++++++++++++++++++++++++++ 3 files changed, 46 insertions(+) diff --git a/src/blocks/sensingblocks.cpp b/src/blocks/sensingblocks.cpp index 2bd2b8564..0238889b3 100644 --- a/src/blocks/sensingblocks.cpp +++ b/src/blocks/sensingblocks.cpp @@ -23,6 +23,7 @@ #include "sensingblocks.h" #include "audio/audioinput.h" #include "audio/iaudioloudness.h" +#include "engine/internal/clock.h" using namespace libscratchcpp; @@ -60,6 +61,7 @@ void SensingBlocks::registerBlocks(IEngine *engine) engine->addCompileFunction(this, "sensing_resettimer", &compileResetTimer); engine->addCompileFunction(this, "sensing_of", &compileOf); engine->addCompileFunction(this, "sensing_current", &compileCurrent); + engine->addCompileFunction(this, "sensing_dayssince2000", &compileDaysSince2000); } void SensingBlocks::onInit(IEngine *engine) @@ -367,6 +369,11 @@ CompilerValue *SensingBlocks::compileCurrent(Compiler *compiler) return compiler->addConstValue(Value()); } +CompilerValue *SensingBlocks::compileDaysSince2000(Compiler *compiler) +{ + return compiler->addFunctionCall("sensing_dayssince2000", Compiler::StaticType::Number); +} + void SensingBlocks::onAnswer(const std::string &answer) { // https://github.com/scratchfoundation/scratch-vm/blob/6055823f203a696165084b873e661713806583ec/src/blocks/scratch3_sensing.js#L99-L115 @@ -775,3 +782,12 @@ extern "C" double sensing_current_second() tm *ltm = localtime(&now); return ltm->tm_sec; } + +extern "C" double sensing_dayssince2000() +{ + if (!SensingBlocks::clock) + SensingBlocks::clock = Clock::instance().get(); + + auto ms = std::chrono::duration_cast(SensingBlocks::clock->currentSystemTime().time_since_epoch()).count(); + return ms / 86400000.0 - 10957.0; +} diff --git a/src/blocks/sensingblocks.h b/src/blocks/sensingblocks.h index e1c10e032..a1d274a2e 100644 --- a/src/blocks/sensingblocks.h +++ b/src/blocks/sensingblocks.h @@ -10,6 +10,7 @@ namespace libscratchcpp { class IAudioInput; +class IClock; class ExecutionContext; class Thread; @@ -27,6 +28,7 @@ class SensingBlocks : public IExtension static void askQuestion(ExecutionContext *ctx, const StringPtr *question); static inline IAudioInput *audioInput = nullptr; + static inline IClock *clock = nullptr; private: struct Question @@ -62,6 +64,7 @@ class SensingBlocks : public IExtension static CompilerValue *compileResetTimer(Compiler *compiler); static CompilerValue *compileOf(Compiler *compiler); static CompilerValue *compileCurrent(Compiler *compiler); + static CompilerValue *compileDaysSince2000(Compiler *compiler); static void onAnswer(const std::string &answer); static void enqueueAsk(const std::string &question, Thread *thread); diff --git a/test/blocks/sensing_blocks_test.cpp b/test/blocks/sensing_blocks_test.cpp index f0c25130b..356b94aca 100644 --- a/test/blocks/sensing_blocks_test.cpp +++ b/test/blocks/sensing_blocks_test.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include "../common.h" #include "blocks/sensingblocks.h" @@ -44,12 +45,15 @@ class SensingBlocksTest : public testing::Test m_audioLoudness = std::make_shared(); SensingBlocks::audioInput = &m_audioInput; EXPECT_CALL(m_audioInput, audioLoudness()).WillRepeatedly(Return(m_audioLoudness)); + + SensingBlocks::clock = &m_clock; } void TearDown() override { SensingBlocks::clearQuestions(); SensingBlocks::audioInput = nullptr; + SensingBlocks::clock = nullptr; } std::unique_ptr m_extension; @@ -57,6 +61,7 @@ class SensingBlocksTest : public testing::Test IEngine *m_engine = nullptr; EngineMock m_engineMock; std::shared_ptr m_audioLoudness; + ClockMock m_clock; private: AudioInputMock m_audioInput; @@ -2745,3 +2750,25 @@ TEST_F(SensingBlocksTest, Current_Invalid) ASSERT_EQ(value_toDouble(&value), 0.0); value_free(&value); } + +TEST_F(SensingBlocksTest, DaysSince2000) +{ + auto targetMock = std::make_shared(); + + ScriptBuilder builder(m_extension.get(), m_engine, targetMock); + builder.addBlock("sensing_dayssince2000"); + Block *block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, targetMock.get()); + auto code = compiler.compile(block, Compiler::CodeType::Reporter); + Script script(targetMock.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(targetMock.get(), &m_engineMock, &script); + + std::chrono::system_clock::time_point time(std::chrono::milliseconds(1011243120562)); // Jan 17 2002 04:52:00 + EXPECT_CALL(m_clock, currentSystemTime()).WillOnce(Return(time)); + + ValueData value = thread.runReporter(); + ASSERT_EQ(value_toDouble(&value), 747.20278428240817); + value_free(&value); +}