From ff78bd4e1f7532c43d7c1094bae4eefb97893395 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Sat, 17 Feb 2024 12:40:13 +0100
Subject: [PATCH 01/77] Add fastBoundingRect() method to ISpriteHandler

---
 include/scratchcpp/ispritehandler.h | 7 +++++++
 test/mocks/spritehandlermock.h      | 2 ++
 2 files changed, 9 insertions(+)

diff --git a/include/scratchcpp/ispritehandler.h b/include/scratchcpp/ispritehandler.h
index 663bffad..d4ec2fe3 100644
--- a/include/scratchcpp/ispritehandler.h
+++ b/include/scratchcpp/ispritehandler.h
@@ -70,6 +70,13 @@ class LIBSCRATCHCPP_EXPORT ISpriteHandler
          * \note The rectangle must be relative to the stage, so make sure to use the sprite's coordinates.
          */
         virtual Rect boundingRect() const = 0;
+
+        /*!
+         * Used to get a less accurate bounding rectangle of the sprite
+         * which is calculated by transforming the costume rectangle.
+         * \note The rectangle must be relative to the stage, so make sure to use the sprite's coordinates.
+         */
+        virtual Rect fastBoundingRect() const = 0;
 };
 
 } // namespace libscratchcpp
diff --git a/test/mocks/spritehandlermock.h b/test/mocks/spritehandlermock.h
index 5b9642ba..93f212f6 100644
--- a/test/mocks/spritehandlermock.h
+++ b/test/mocks/spritehandlermock.h
@@ -1,6 +1,7 @@
 #pragma once
 
 #include <scratchcpp/ispritehandler.h>
+#include <scratchcpp/rect.h>
 #include <gmock/gmock.h>
 
 using namespace libscratchcpp;
@@ -29,4 +30,5 @@ class SpriteHandlerMock : public ISpriteHandler
         MOCK_METHOD(void, onBubbleTextChanged, (const std::string &), (override));
 
         MOCK_METHOD(Rect, boundingRect, (), (const, override));
+        MOCK_METHOD(Rect, fastBoundingRect, (), (const, override));
 };

From 7e2ab27c158bf555b93dd6627a062927636305a5 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Sat, 17 Feb 2024 12:51:12 +0100
Subject: [PATCH 02/77] Add fastBoundingRect() method to Sprite

---
 include/scratchcpp/sprite.h                    |  1 +
 src/scratch/sprite.cpp                         | 12 ++++++++++++
 test/scratch_classes/sprite_test.cpp           | 12 ++++++++++++
 test/target_interfaces/ispritehandler_test.cpp | 10 ++++++++++
 4 files changed, 35 insertions(+)

diff --git a/include/scratchcpp/sprite.h b/include/scratchcpp/sprite.h
index 23191429..616c5636 100644
--- a/include/scratchcpp/sprite.h
+++ b/include/scratchcpp/sprite.h
@@ -70,6 +70,7 @@ class LIBSCRATCHCPP_EXPORT Sprite
         void setLayerOrder(int newLayerOrder) override;
 
         Rect boundingRect() const;
+        Rect fastBoundingRect() const;
         void keepInFence(double newX, double newY, double *fencedX, double *fencedY) const;
 
         void setGraphicsEffectValue(IGraphicsEffect *effect, double value) override;
diff --git a/src/scratch/sprite.cpp b/src/scratch/sprite.cpp
index 609241af..62043a95 100644
--- a/src/scratch/sprite.cpp
+++ b/src/scratch/sprite.cpp
@@ -350,6 +350,18 @@ Rect Sprite::boundingRect() const
     return impl->iface->boundingRect();
 }
 
+/*!
+ * Returns the less accurate bounding rectangle of the sprite
+ * which is calculated by transforming the costume rectangle.
+ */
+Rect Sprite::fastBoundingRect() const
+{
+    if (!impl->iface)
+        return Rect(impl->x, impl->y, impl->x, impl->y);
+
+    return impl->iface->fastBoundingRect();
+}
+
 /*!
  * Keeps the desired position within the stage.
  * \param[in] New desired X position.
diff --git a/test/scratch_classes/sprite_test.cpp b/test/scratch_classes/sprite_test.cpp
index 0d9f1067..59815ffb 100644
--- a/test/scratch_classes/sprite_test.cpp
+++ b/test/scratch_classes/sprite_test.cpp
@@ -602,6 +602,18 @@ TEST(SpriteTest, DefaultBoundingRect)
     ASSERT_EQ(rect.bottom(), -45.01);
 }
 
+TEST(SpriteTest, DefaultFastBoundingRect)
+{
+    Sprite sprite;
+    sprite.setX(65.5);
+    sprite.setY(-45.01);
+    Rect rect = sprite.fastBoundingRect();
+    ASSERT_EQ(rect.left(), 65.5);
+    ASSERT_EQ(rect.top(), -45.01);
+    ASSERT_EQ(rect.right(), 65.5);
+    ASSERT_EQ(rect.bottom(), -45.01);
+}
+
 TEST(SpriteTest, GraphicsEffects)
 {
     Sprite sprite;
diff --git a/test/target_interfaces/ispritehandler_test.cpp b/test/target_interfaces/ispritehandler_test.cpp
index 777fb2ed..bbcf11e0 100644
--- a/test/target_interfaces/ispritehandler_test.cpp
+++ b/test/target_interfaces/ispritehandler_test.cpp
@@ -199,3 +199,13 @@ TEST_F(ISpriteHandlerTest, BoundingRect)
     ASSERT_EQ(rect.right(), 20.5);
     ASSERT_EQ(rect.bottom(), -0.48);
 }
+
+TEST_F(ISpriteHandlerTest, FastBoundingRect)
+{
+    EXPECT_CALL(m_handler, fastBoundingRect()).WillOnce(Return(Rect(-44.6, 89.1, 20.5, -0.48)));
+    Rect rect = m_sprite.fastBoundingRect();
+    ASSERT_EQ(rect.left(), -44.6);
+    ASSERT_EQ(rect.top(), 89.1);
+    ASSERT_EQ(rect.right(), 20.5);
+    ASSERT_EQ(rect.bottom(), -0.48);
+}

From 68a46f308b994fbbee716be5c14ff3f483bd6b69 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Sat, 17 Feb 2024 12:55:49 +0100
Subject: [PATCH 03/77] fix #469: Use fast bounding rect for sprite fencing

---
 src/scratch/sprite_p.cpp                       |  4 +++-
 test/scratch_classes/sprite_test.cpp           | 16 ++++++++--------
 test/target_interfaces/ispritehandler_test.cpp |  4 ++--
 3 files changed, 13 insertions(+), 11 deletions(-)

diff --git a/src/scratch/sprite_p.cpp b/src/scratch/sprite_p.cpp
index 3f92897e..37dfc591 100644
--- a/src/scratch/sprite_p.cpp
+++ b/src/scratch/sprite_p.cpp
@@ -48,7 +48,9 @@ void SpritePrivate::getFencedPosition(double x, double y, double *outX, double *
     Rect rect;
 
     if (iface)
-        rect = iface->boundingRect();
+        rect = iface->fastBoundingRect();
+    else
+        rect = Rect(this->x, this->y, this->x, this->y);
 
     double inset = std::floor(std::min(rect.width(), rect.height()) / 2);
 
diff --git a/test/scratch_classes/sprite_test.cpp b/test/scratch_classes/sprite_test.cpp
index 59815ffb..04c15da1 100644
--- a/test/scratch_classes/sprite_test.cpp
+++ b/test/scratch_classes/sprite_test.cpp
@@ -271,7 +271,7 @@ TEST(SpriteTest, XY)
     EXPECT_CALL(engine, spriteFencingEnabled()).WillOnce(Return(true));
     EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480));
     EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360));
-    EXPECT_CALL(handler, boundingRect()).WillOnce(Return(rect));
+    EXPECT_CALL(handler, fastBoundingRect()).WillOnce(Return(rect));
     EXPECT_CALL(handler, onMoved);
     EXPECT_CALL(handler, onXChanged);
     sprite.setX(319);
@@ -280,7 +280,7 @@ TEST(SpriteTest, XY)
     EXPECT_CALL(engine, spriteFencingEnabled()).WillOnce(Return(true));
     EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480));
     EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360));
-    EXPECT_CALL(handler, boundingRect()).WillOnce(Return(rect));
+    EXPECT_CALL(handler, fastBoundingRect()).WillOnce(Return(rect));
     EXPECT_CALL(handler, onMoved);
     EXPECT_CALL(handler, onXChanged);
     sprite.setX(75);
@@ -289,7 +289,7 @@ TEST(SpriteTest, XY)
     EXPECT_CALL(engine, spriteFencingEnabled()).WillOnce(Return(true));
     EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480));
     EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360));
-    EXPECT_CALL(handler, boundingRect()).WillOnce(Return(rect));
+    EXPECT_CALL(handler, fastBoundingRect()).WillOnce(Return(rect));
     EXPECT_CALL(handler, onMoved);
     EXPECT_CALL(handler, onXChanged);
     sprite.setX(400);
@@ -304,7 +304,7 @@ TEST(SpriteTest, XY)
     EXPECT_CALL(engine, spriteFencingEnabled()).WillOnce(Return(true));
     EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480));
     EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360));
-    EXPECT_CALL(handler, boundingRect()).WillOnce(Return(rect));
+    EXPECT_CALL(handler, fastBoundingRect()).WillOnce(Return(rect));
     EXPECT_CALL(handler, onMoved);
     EXPECT_CALL(handler, onXChanged);
     sprite.setX(-400);
@@ -319,7 +319,7 @@ TEST(SpriteTest, XY)
     EXPECT_CALL(engine, spriteFencingEnabled()).WillOnce(Return(true));
     EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480));
     EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360));
-    EXPECT_CALL(handler, boundingRect()).WillOnce(Return(rect));
+    EXPECT_CALL(handler, fastBoundingRect()).WillOnce(Return(rect));
     EXPECT_CALL(handler, onMoved);
     EXPECT_CALL(handler, onYChanged);
     sprite.setY(150);
@@ -328,7 +328,7 @@ TEST(SpriteTest, XY)
     EXPECT_CALL(engine, spriteFencingEnabled()).WillOnce(Return(true));
     EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480));
     EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360));
-    EXPECT_CALL(handler, boundingRect()).WillOnce(Return(rect));
+    EXPECT_CALL(handler, fastBoundingRect()).WillOnce(Return(rect));
     EXPECT_CALL(handler, onMoved);
     EXPECT_CALL(handler, onYChanged);
     sprite.setY(-103);
@@ -337,7 +337,7 @@ TEST(SpriteTest, XY)
     EXPECT_CALL(engine, spriteFencingEnabled()).WillOnce(Return(true));
     EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480));
     EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360));
-    EXPECT_CALL(handler, boundingRect()).WillOnce(Return(rect));
+    EXPECT_CALL(handler, fastBoundingRect()).WillOnce(Return(rect));
     EXPECT_CALL(handler, onMoved);
     EXPECT_CALL(handler, onYChanged);
     sprite.setY(340);
@@ -352,7 +352,7 @@ TEST(SpriteTest, XY)
     EXPECT_CALL(engine, spriteFencingEnabled()).WillOnce(Return(true));
     EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480));
     EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360));
-    EXPECT_CALL(handler, boundingRect()).WillOnce(Return(rect));
+    EXPECT_CALL(handler, fastBoundingRect()).WillOnce(Return(rect));
     EXPECT_CALL(handler, onMoved);
     EXPECT_CALL(handler, onYChanged);
     sprite.setY(-340);
diff --git a/test/target_interfaces/ispritehandler_test.cpp b/test/target_interfaces/ispritehandler_test.cpp
index bbcf11e0..f80772f1 100644
--- a/test/target_interfaces/ispritehandler_test.cpp
+++ b/test/target_interfaces/ispritehandler_test.cpp
@@ -65,7 +65,7 @@ TEST_F(ISpriteHandlerTest, X)
 {
     EXPECT_CALL(m_handler, onMoved(0, 0, 189.46, 0));
     EXPECT_CALL(m_handler, onXChanged(189.46)).Times(1);
-    EXPECT_CALL(m_handler, boundingRect()).WillOnce(Return(Rect()));
+    EXPECT_CALL(m_handler, fastBoundingRect()).WillOnce(Return(Rect()));
     EXPECT_CALL(m_engine, spriteFencingEnabled()).WillOnce(Return(true));
     EXPECT_CALL(m_engine, stageWidth()).WillOnce(Return(480));
     EXPECT_CALL(m_engine, stageHeight()).WillOnce(Return(360));
@@ -83,7 +83,7 @@ TEST_F(ISpriteHandlerTest, Y)
 {
     EXPECT_CALL(m_handler, onMoved(0, 0, 0, -153.7)).Times(1);
     EXPECT_CALL(m_handler, onYChanged(-153.7)).Times(1);
-    EXPECT_CALL(m_handler, boundingRect()).WillOnce(Return(Rect()));
+    EXPECT_CALL(m_handler, fastBoundingRect()).WillOnce(Return(Rect()));
     EXPECT_CALL(m_engine, spriteFencingEnabled()).WillOnce(Return(true));
     EXPECT_CALL(m_engine, stageWidth()).WillOnce(Return(480));
     EXPECT_CALL(m_engine, stageHeight()).WillOnce(Return(360));

From 4a2f62aed7b172a19d09e72443ecdb66725cb928 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Sat, 17 Feb 2024 13:37:27 +0100
Subject: [PATCH 04/77] fix #488: Use Value constructor for numbers in
 jsonToValue()

---
 src/internal/reader_common.h                  |  13 +--------
 test/load_project/load_project_test.cpp       |  25 +++++++++++++++---
 .../488_scientific_notation_conversion.sb3    | Bin 0 -> 797 bytes
 3 files changed, 23 insertions(+), 15 deletions(-)
 create mode 100644 test/regtest_projects/488_scientific_notation_conversion.sb3

diff --git a/src/internal/reader_common.h b/src/internal/reader_common.h
index 09815d01..840a1aa3 100644
--- a/src/internal/reader_common.h
+++ b/src/internal/reader_common.h
@@ -13,18 +13,7 @@ Value jsonToValue(nlohmann::json value)
 {
     if (value.is_string())
         return value.get<std::string>();
-    else if (value.is_number()) {
-        // Set locale to C to avoid conversion issues
-        std::string oldLocale = std::setlocale(LC_NUMERIC, nullptr);
-        std::setlocale(LC_NUMERIC, "C");
-
-        double converted = std::stod(value.dump());
-
-        // Restore old locale
-        std::setlocale(LC_NUMERIC, oldLocale.c_str());
-
-        return converted;
-    } else if (value.is_boolean())
+    else if (value.is_boolean())
         return value.get<bool>();
     else
         return value.dump();
diff --git a/test/load_project/load_project_test.cpp b/test/load_project/load_project_test.cpp
index dbacec3d..767a5feb 100644
--- a/test/load_project/load_project_test.cpp
+++ b/test/load_project/load_project_test.cpp
@@ -231,7 +231,8 @@ TEST(LoadProjectTest, LoadTestProject)
 
         // Stage: lists
         ASSERT_LIST(stage, "list1");
-        ASSERT_EQ(GET_LIST(stage, "list1")->toString(), "3 1 4 8 7 6 7 7 2 4");
+        ASSERT_EQ(GET_LIST(stage, "list1")->size(), 10);
+        ASSERT_EQ(GET_LIST(stage, "list1")->toString(), "3148767724");
 
         // Sprite1
         ASSERT_NE(engine->findTarget("Sprite1"), -1);
@@ -282,7 +283,8 @@ TEST(LoadProjectTest, LoadTestProject)
 
         // Sprite1: lists
         ASSERT_LIST(sprite1, "list2");
-        ASSERT_EQ(GET_LIST(sprite1, "list2")->toString(), "9 6 3 8 0 4 4 2 9 7");
+        ASSERT_EQ(GET_LIST(sprite1, "list2")->size(), 10);
+        ASSERT_EQ(GET_LIST(sprite1, "list2")->toString(), "9638044297");
 
         // Sprite1: blocks
         ASSERT_EQ(sprite1->greenFlagBlocks().size(), 1);
@@ -445,7 +447,8 @@ TEST(LoadProjectTest, LoadTestProject)
 
         // Balloon1: lists
         ASSERT_LIST(sprite2, "list2");
-        ASSERT_EQ(GET_LIST(sprite2, "list2")->toString(), "0 4 3 4 1 5 6 9 4 8");
+        ASSERT_EQ(GET_LIST(sprite2, "list2")->size(), 10);
+        ASSERT_EQ(GET_LIST(sprite2, "list2")->toString(), "0434156948");
 
         // Monitors
         const auto &monitors = engine->monitors();
@@ -740,3 +743,19 @@ TEST(LoadProjectTest, LoadDoubleValue)
 
     std::setlocale(LC_NUMERIC, oldLocale.c_str());
 }
+
+TEST(LoadProjectTest, LoadScientificNotationNumber)
+{
+    // Regtest for #488
+    std::string name = "regtest_projects/488_scientific_notation_conversion.sb3";
+    Project p(name);
+    ASSERT_TRUE(p.load());
+
+    auto stage = p.engine()->stage();
+    ASSERT_TRUE(stage);
+    ASSERT_VAR(stage, "test1");
+    ASSERT_EQ(GET_VAR(stage, "test1")->value().toDouble(), 5000);
+
+    ASSERT_VAR(stage, "test2");
+    ASSERT_EQ(GET_VAR(stage, "test2")->value().toDouble(), 0);
+}
diff --git a/test/regtest_projects/488_scientific_notation_conversion.sb3 b/test/regtest_projects/488_scientific_notation_conversion.sb3
new file mode 100644
index 0000000000000000000000000000000000000000..1c9001fc0ace918a6dceb356f69219cd7472cd98
GIT binary patch

From 2e3d3df69476937958a83a01747c0c7c38a48f42 Mon Sep 17 00:00:00 2001
From: Jessie YU <jy2728@nyu.edu>
Date: Fri, 23 Feb 2024 09:46:45 -0500
Subject: [PATCH 05/77] Fix #493: Ignore leading/trailing spaces in url (#494)

---
 src/internal/projecturl.cpp      | 14 ++++++++++++++
 test/network/projecturl_test.cpp | 19 +++++++++++++++++++
 2 files changed, 33 insertions(+)

diff --git a/src/internal/projecturl.cpp b/src/internal/projecturl.cpp
index 9e3a6514..88ce4c25 100644
--- a/src/internal/projecturl.cpp
+++ b/src/internal/projecturl.cpp
@@ -22,6 +22,20 @@ ProjectUrl::ProjectUrl(const std::string &url)
 
     if (parts.empty())
         return;
+    
+    // trim leading whitespaces
+    str = parts.front();
+    str.erase(str.begin(), std::find_if(str.begin(), str.end(), [](unsigned char ch) {
+        return !std::isspace(ch);
+    }));
+    parts[0] = str;
+
+    // trim trailing whitespaces
+    str = parts.back();
+    str.erase(std::find_if(str.rbegin(), str.rend(), [](int ch){
+        return !std::isspace(ch);
+        }).base(), str.end());
+    parts[parts.size() - 1] = str;
 
     // Remove empty parts
     parts.erase(std::remove(parts.begin(), parts.end(), ""), parts.end());
diff --git a/test/network/projecturl_test.cpp b/test/network/projecturl_test.cpp
index 60a13d5b..eea1cc0d 100644
--- a/test/network/projecturl_test.cpp
+++ b/test/network/projecturl_test.cpp
@@ -59,4 +59,23 @@ TEST(ProjectUrlTest, Parse)
         ASSERT_FALSE(url.isProjectUrl());
         ASSERT_TRUE(url.projectId().empty());
     }
+
+    {
+        ProjectUrl url("    https:/scratch.mit.edu/projects/886166351");
+        ASSERT_TRUE(url.isProjectUrl());
+        ASSERT_EQ(url.projectId(), "886166351");
+    }
+
+    {
+        ProjectUrl url("https:/scratch.mit.edu/projects/886166351    ");
+        ASSERT_TRUE(url.isProjectUrl());
+        ASSERT_EQ(url.projectId(), "886166351");
+    }
+
+    {
+        ProjectUrl url(" https:/scratch.mit.edu/projects/886166351  ");
+        ASSERT_TRUE(url.isProjectUrl());
+        ASSERT_EQ(url.projectId(), "886166351");
+    }
+    
 }

From eaa66eaca3138ac01558c4941cec37f3fc60dc46 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Sat, 24 Feb 2024 20:53:59 +0100
Subject: [PATCH 06/77] Use FetchContent to fetch nlohmann_json

---
 CMakeLists.txt | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 4dcfec42..e3ee8ee9 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -16,7 +16,6 @@ if (NOT (CMAKE_CXX_COMPILER_ID STREQUAL "GNU"))
 	set(LIBSCRATCHCPP_COMPUTED_GOTO OFF CACHE BOOL "" FORCE)
 endif()
 
-find_package(nlohmann_json 3.9.1 REQUIRED)
 find_package(utf8cpp REQUIRED)
 
 add_library(scratchcpp SHARED)
@@ -66,6 +65,8 @@ target_sources(scratchcpp
     include/scratchcpp/imonitorhandler.h
 )
 
+include(FetchContent)
+
 add_library(zip SHARED
 	${ZIP_SRC}/zip.c
 	${ZIP_SRC}/zip.h
@@ -75,13 +76,15 @@ include_directories(thirdparty/zip/src)
 
 include_directories(thirdparty/spimpl)
 
+FetchContent_Declare(json URL https://github.com/nlohmann/json/releases/download/v3.11.3/json.tar.xz)
+FetchContent_MakeAvailable(json)
+
 target_link_libraries(scratchcpp PRIVATE nlohmann_json::nlohmann_json)
 target_link_libraries(scratchcpp PRIVATE utf8cpp)
 target_link_libraries(scratchcpp PRIVATE zip)
 target_link_libraries(scratchcpp PRIVATE scratchcpp-audio)
 
 if (LIBSCRATCHCPP_NETWORK_SUPPORT)
-    include(FetchContent)
     FetchContent_Declare(cpr GIT_REPOSITORY https://github.com/libcpr/cpr.git
 	                     GIT_TAG 3b15fa82ea74739b574d705fea44959b58142eb8) # 1.10.5
     FetchContent_MakeAvailable(cpr)

From 73aab331296253b35262e466d74cb86133f85ca7 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Sat, 24 Feb 2024 23:00:08 +0100
Subject: [PATCH 07/77] Add utfcpp submdule

---
 .gitmodules       | 3 +++
 CMakeLists.txt    | 9 ++++-----
 thirdparty/utfcpp | 1 +
 3 files changed, 8 insertions(+), 5 deletions(-)
 create mode 160000 thirdparty/utfcpp

diff --git a/.gitmodules b/.gitmodules
index 9a419fc7..1d99c252 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -4,3 +4,6 @@
 [submodule "thirdparty/googletest"]
 	path = thirdparty/googletest
 	url = https://github.com/google/googletest
+[submodule "thirdparty/utfcpp"]
+	path = thirdparty/utfcpp
+	url = https://github.com/nemtrif/utfcpp
diff --git a/CMakeLists.txt b/CMakeLists.txt
index e3ee8ee9..57f65922 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -5,7 +5,6 @@ project(libscratchcpp LANGUAGES C CXX)
 set(CMAKE_INCLUDE_CURRENT_DIR ON)
 set(CMAKE_CXX_STANDARD 17)
 set(CMAKE_CXX_STANDARD_REQUIRED ON)
-set(ZIP_SRC thirdparty/zip/src)
 
 option(LIBSCRATCHCPP_BUILD_UNIT_TESTS "Build unit tests" ON)
 option(LIBSCRATCHCPP_NETWORK_SUPPORT "Support for downloading projects" ON)
@@ -16,8 +15,6 @@ if (NOT (CMAKE_CXX_COMPILER_ID STREQUAL "GNU"))
 	set(LIBSCRATCHCPP_COMPUTED_GOTO OFF CACHE BOOL "" FORCE)
 endif()
 
-find_package(utf8cpp REQUIRED)
-
 add_library(scratchcpp SHARED)
 add_subdirectory(src)
 include_directories(src) # TODO: Remove this line
@@ -66,21 +63,23 @@ target_sources(scratchcpp
 )
 
 include(FetchContent)
+set(ZIP_SRC thirdparty/zip/src)
+set(UTFCPP_SRC thirdparty/utfcpp/source)
 
 add_library(zip SHARED
 	${ZIP_SRC}/zip.c
 	${ZIP_SRC}/zip.h
 	${ZIP_SRC}/miniz.h
 )
-include_directories(thirdparty/zip/src)
+target_include_directories(scratchcpp PUBLIC ${ZIP_SRC})
 
+target_include_directories(scratchcpp PUBLIC ${UTFCPP_SRC})
 include_directories(thirdparty/spimpl)
 
 FetchContent_Declare(json URL https://github.com/nlohmann/json/releases/download/v3.11.3/json.tar.xz)
 FetchContent_MakeAvailable(json)
 
 target_link_libraries(scratchcpp PRIVATE nlohmann_json::nlohmann_json)
-target_link_libraries(scratchcpp PRIVATE utf8cpp)
 target_link_libraries(scratchcpp PRIVATE zip)
 target_link_libraries(scratchcpp PRIVATE scratchcpp-audio)
 
diff --git a/thirdparty/utfcpp b/thirdparty/utfcpp
new file mode 160000
index 00000000..6be08bbe
--- /dev/null
+++ b/thirdparty/utfcpp
@@ -0,0 +1 @@
+Subproject commit 6be08bbea14ffa0a5c594257fb6285a054395cd7

From 8dc24ae9bf1d89f8c11b7920c88e46dcf82fe90a Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Tue, 27 Feb 2024 11:46:08 +0100
Subject: [PATCH 08/77] fix #497: Stop VM execution after thread kills itself

---
 src/engine/virtualmachine_p.cpp               |  8 +++++++
 test/virtual_machine/virtual_machine_test.cpp | 21 +++++++++++++++++++
 2 files changed, 29 insertions(+)

diff --git a/src/engine/virtualmachine_p.cpp b/src/engine/virtualmachine_p.cpp
index d6c93591..732666cb 100644
--- a/src/engine/virtualmachine_p.cpp
+++ b/src/engine/virtualmachine_p.cpp
@@ -781,6 +781,14 @@ unsigned int *VirtualMachinePrivate::run(unsigned int *pos, bool reset)
             DISPATCH(); // this avoids freeing registers after "stopping" a warp script
         }
         FREE_REGS(ret);
+
+        if (atEnd) {
+            if (regCount > 0)
+                std::cout << "warning: VM: " << regCount << " registers were leaked by the script; this is most likely a bug in the VM or in the compiler" << std::endl;
+
+            return pos;
+        }
+
         DISPATCH();
     }
 
diff --git a/test/virtual_machine/virtual_machine_test.cpp b/test/virtual_machine/virtual_machine_test.cpp
index 27819ba2..28167d6d 100644
--- a/test/virtual_machine/virtual_machine_test.cpp
+++ b/test/virtual_machine/virtual_machine_test.cpp
@@ -1773,3 +1773,24 @@ TEST(VirtualMachineTest, NoCrashWhenReadingProcedureArgsAfterStopping)
     ASSERT_EQ(vm.registerCount(), 0);
     ASSERT_TRUE(vm.atEnd());
 }
+
+unsigned int killTest(VirtualMachine *vm)
+{
+    vm->kill();
+    return 1;
+}
+
+TEST(VirtualMachineTest, StopAfterKilled)
+{
+    // Regtest for #497
+    static unsigned int bytecode[] = { OP_START, OP_NULL, OP_EXEC, 0, OP_NULL, OP_NULL, OP_HALT };
+    static BlockFunc functions[] = { &killTest };
+
+    VirtualMachine vm;
+    vm.setBytecode(bytecode);
+    vm.setFunctions(functions);
+
+    vm.run();
+    ASSERT_TRUE(vm.atEnd());
+    ASSERT_EQ(vm.registerCount(), 0);
+}

From 2feb8007262b024b0c1f36575739d0e0d12d8a97 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Tue, 27 Feb 2024 12:29:33 +0100
Subject: [PATCH 09/77] fix #498: Store a tree of running loop count

---
 src/engine/virtualmachine_p.cpp               | 11 ++++++++++
 src/engine/virtualmachine_p.h                 |  1 +
 test/virtual_machine/virtual_machine_test.cpp | 20 +++++++++++++++++++
 3 files changed, 32 insertions(+)

diff --git a/src/engine/virtualmachine_p.cpp b/src/engine/virtualmachine_p.cpp
index 732666cb..a1e3a7f2 100644
--- a/src/engine/virtualmachine_p.cpp
+++ b/src/engine/virtualmachine_p.cpp
@@ -121,6 +121,7 @@ VirtualMachinePrivate::VirtualMachinePrivate(VirtualMachine *vm, Target *target,
     regs = regsVector.data();
     loops.reserve(256);
     callTree.reserve(1024);
+    loopCountTree.reserve(1024);
 
     if (!rng)
         rng = RandomGenerator::instance().get();
@@ -233,10 +234,19 @@ unsigned int *VirtualMachinePrivate::run(unsigned int *pos, bool reset)
         warp = oldState.second;
         callTree.pop_back();
         procedureArgTree.pop_back();
+
         if (procedureArgTree.empty())
             procedureArgs = nullptr;
         else
             procedureArgs = &procedureArgTree.back();
+
+        int loopsToRemove = loops.size() - loopCountTree.back();
+        assert(loopsToRemove >= 0);
+        loopCountTree.pop_back();
+
+        for (int i = 0; i < loopsToRemove; i++)
+            loops.pop_back();
+
         DISPATCH();
     }
 
@@ -807,6 +817,7 @@ unsigned int *VirtualMachinePrivate::run(unsigned int *pos, bool reset)
             callTree.push_back({ ++pos, warp });
             procedureArgs = nextProcedureArgs;
             nextProcedureArgs = nullptr;
+            loopCountTree.push_back(loops.size());
             pos = procedurePos;
         } else
             pos++;
diff --git a/src/engine/virtualmachine_p.h b/src/engine/virtualmachine_p.h
index 7b1f1c6c..5ee86cf0 100644
--- a/src/engine/virtualmachine_p.h
+++ b/src/engine/virtualmachine_p.h
@@ -50,6 +50,7 @@ struct VirtualMachinePrivate
         std::vector<std::vector<Value>> procedureArgTree;
         std::vector<Value> *procedureArgs = nullptr;
         std::vector<Value> *nextProcedureArgs = nullptr;
+        std::vector<unsigned int> loopCountTree;
         bool noBreak = true;
         bool warp = false;
         bool stop = false;
diff --git a/test/virtual_machine/virtual_machine_test.cpp b/test/virtual_machine/virtual_machine_test.cpp
index 28167d6d..6f63e6c5 100644
--- a/test/virtual_machine/virtual_machine_test.cpp
+++ b/test/virtual_machine/virtual_machine_test.cpp
@@ -1794,3 +1794,23 @@ TEST(VirtualMachineTest, StopAfterKilled)
     ASSERT_TRUE(vm.atEnd());
     ASSERT_EQ(vm.registerCount(), 0);
 }
+
+TEST(VirtualMachineTest, NoCrashAfterCallingProcedureFromLoop)
+{
+    // Regtest for #498
+    static unsigned int bytecode[] = { OP_START, OP_CONST, 0, OP_REPEAT_LOOP, OP_INIT_PROCEDURE, OP_CONST, 1, OP_ADD_ARG, OP_CALL_PROCEDURE, 0, OP_LOOP_END, OP_HALT };
+    static unsigned int procedure[] = { OP_START, OP_WARP, OP_CONST, 0, OP_REPEAT_LOOP, OP_READ_ARG, 0, OP_PRINT, OP_HALT, OP_LOOP_END, OP_HALT };
+    static unsigned int *procedures[] = { procedure };
+    static Value constValues[] = { 2, "test" };
+
+    VirtualMachine vm;
+    vm.setBytecode(bytecode);
+    vm.setProcedures(procedures);
+    vm.setConstValues(constValues);
+
+    ::testing::internal::CaptureStdout();
+    vm.run();
+    ASSERT_TRUE(vm.atEnd());
+    ASSERT_EQ(vm.registerCount(), 0);
+    ASSERT_EQ(::testing::internal::GetCapturedStdout(), "test\ntest\n");
+}

From 2e7e50a80db9893b18f47a45b503ccd2ea8ede2e Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Fri, 1 Mar 2024 23:20:29 +0100
Subject: [PATCH 10/77] fix #503: Use signed integer for loop count

---
 src/engine/virtualmachine_p.cpp               |  2 +-
 test/virtual_machine/virtual_machine_test.cpp | 17 +++++++++++++++++
 2 files changed, 18 insertions(+), 1 deletion(-)

diff --git a/src/engine/virtualmachine_p.cpp b/src/engine/virtualmachine_p.cpp
index a1e3a7f2..cdbc5c9a 100644
--- a/src/engine/virtualmachine_p.cpp
+++ b/src/engine/virtualmachine_p.cpp
@@ -206,7 +206,7 @@ unsigned int *VirtualMachinePrivate::run(unsigned int *pos, bool reset)
     assert(pos);
     unsigned int *loopStart;
     unsigned int *loopEnd;
-    size_t loopCount;
+    long loopCount;
     if (reset) {
         atEnd = false;
         noBreak = true;
diff --git a/test/virtual_machine/virtual_machine_test.cpp b/test/virtual_machine/virtual_machine_test.cpp
index 6f63e6c5..9308116e 100644
--- a/test/virtual_machine/virtual_machine_test.cpp
+++ b/test/virtual_machine/virtual_machine_test.cpp
@@ -1814,3 +1814,20 @@ TEST(VirtualMachineTest, NoCrashAfterCallingProcedureFromLoop)
     ASSERT_EQ(vm.registerCount(), 0);
     ASSERT_EQ(::testing::internal::GetCapturedStdout(), "test\ntest\n");
 }
+
+TEST(VirtualMachineTest, NegativeLoopCount)
+{
+    // Regtest for #503
+    static unsigned int bytecode[] = { OP_START, OP_CONST, 0, OP_REPEAT_LOOP, OP_CONST, 1, OP_PRINT, OP_BREAK_FRAME, OP_LOOP_END, OP_HALT };
+    static Value constValues[] = { -1, "test" };
+
+    VirtualMachine vm;
+    vm.setBytecode(bytecode);
+    vm.setConstValues(constValues);
+
+    ::testing::internal::CaptureStdout();
+    vm.run();
+    ASSERT_TRUE(::testing::internal::GetCapturedStdout().empty());
+    ASSERT_TRUE(vm.atEnd());
+    ASSERT_EQ(vm.registerCount(), 0);
+}

From cb7a932a347c56881549ffcdd0044e2baa4ecabc Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Fri, 1 Mar 2024 23:28:19 +0100
Subject: [PATCH 11/77] fix #502: Ignore hidden sprites in
 Engine::clickTarget()

---
 src/engine/internal/engine.cpp |   9 ++++++++-
 test/engine/engine_test.cpp    |  25 +++++++++++++++++++++++++
 test/target_click_scripts.sb3  | Bin 1566 -> 1690 bytes
 3 files changed, 33 insertions(+), 1 deletion(-)

diff --git a/src/engine/internal/engine.cpp b/src/engine/internal/engine.cpp
index 8734cc41..6ab9c3d1 100644
--- a/src/engine/internal/engine.cpp
+++ b/src/engine/internal/engine.cpp
@@ -695,7 +695,14 @@ void Engine::setMousePressed(bool pressed)
 
 void Engine::clickTarget(Target *target)
 {
-    startHats(HatType::TargetClicked, {}, target);
+    assert(target);
+
+    if (target) {
+        if (!target->isStage() && !static_cast<Sprite *>(target)->visible())
+            return;
+
+        startHats(HatType::TargetClicked, {}, target);
+    }
 }
 
 unsigned int Engine::stageWidth() const
diff --git a/test/engine/engine_test.cpp b/test/engine/engine_test.cpp
index 73d83254..0fdc8dc4 100644
--- a/test/engine/engine_test.cpp
+++ b/test/engine/engine_test.cpp
@@ -1124,6 +1124,31 @@ TEST(EngineTest, TargetClickScripts)
     ASSERT_VAR(stage, "stage");
     ASSERT_EQ(GET_VAR(stage, "stage")->value().toInt(), 0);
 
+    // Hidden sprite
+    sprite = engine->targetAt(engine->findTarget("Hidden"));
+    ASSERT_TRUE(sprite);
+    engine->clickTarget(sprite);
+    engine->step();
+    ASSERT_VAR(stage, "1");
+    ASSERT_EQ(GET_VAR(stage, "1")->value().toInt(), 2);
+    ASSERT_VAR(stage, "2");
+    ASSERT_EQ(GET_VAR(stage, "2")->value().toInt(), 2);
+    ASSERT_VAR(stage, "hidden");
+    ASSERT_EQ(GET_VAR(stage, "hidden")->value().toInt(), 0);
+    ASSERT_VAR(stage, "stage");
+    ASSERT_EQ(GET_VAR(stage, "stage")->value().toInt(), 0);
+
+    engine->clickTarget(sprite);
+    engine->step();
+    ASSERT_VAR(stage, "1");
+    ASSERT_EQ(GET_VAR(stage, "1")->value().toInt(), 2);
+    ASSERT_VAR(stage, "2");
+    ASSERT_EQ(GET_VAR(stage, "2")->value().toInt(), 2);
+    ASSERT_VAR(stage, "hidden");
+    ASSERT_EQ(GET_VAR(stage, "hidden")->value().toInt(), 0);
+    ASSERT_VAR(stage, "stage");
+    ASSERT_EQ(GET_VAR(stage, "stage")->value().toInt(), 0);
+
     // Stage
     engine->clickTarget(stage);
     engine->step();
diff --git a/test/target_click_scripts.sb3 b/test/target_click_scripts.sb3
index ba8a0233c14ce53c3225443fa66875a67fa5e540..c30acb87b75e7c788a3e1ed23d2dd2117c13baf5 100644
GIT binary patch

From 58c417c4db7000971efda838664961c2a6df271c Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Tue, 5 Mar 2024 18:16:43 +0100
Subject: [PATCH 12/77] Add definitions for project version

---
 CMakeLists.txt | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 57f65922..3d9a8456 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,6 +1,6 @@
 cmake_minimum_required(VERSION 3.14)
 
-project(libscratchcpp LANGUAGES C CXX)
+project(libscratchcpp VERSION 0.9.0 LANGUAGES C CXX)
 
 set(CMAKE_INCLUDE_CURRENT_DIR ON)
 set(CMAKE_CXX_STANDARD 17)
@@ -92,6 +92,10 @@ if (LIBSCRATCHCPP_NETWORK_SUPPORT)
 endif()
 
 target_compile_definitions(scratchcpp PRIVATE LIBSCRATCHCPP_LIBRARY)
+target_compile_definitions(scratchcpp PRIVATE LIBSCRATCHCPP_VERSION="${PROJECT_VERSION}")
+target_compile_definitions(scratchcpp PRIVATE LIBSCRATCHCPP_VERSION_MAJOR=${PROJECT_VERSION_MAJOR})
+target_compile_definitions(scratchcpp PRIVATE LIBSCRATCHCPP_VERSION_MINOR=${PROJECT_VERSION_MINOR})
+target_compile_definitions(scratchcpp PRIVATE LIBSCRATCHCPP_VERSION_PATCH=${PROJECT_VERSION_PATCH})
 
 if (LIBSCRATCHCPP_BUILD_UNIT_TESTS)
     enable_testing()

From 36dccb86cd84656c58371baca3ef9b25a0d4a6c6 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Tue, 5 Mar 2024 18:17:18 +0100
Subject: [PATCH 13/77] Add version API

---
 include/scratchcpp/scratchconfiguration.h |  5 +++++
 src/scratchconfiguration.cpp              | 25 +++++++++++++++++++++++
 2 files changed, 30 insertions(+)

diff --git a/include/scratchcpp/scratchconfiguration.h b/include/scratchcpp/scratchconfiguration.h
index 46414e7b..991bdcdc 100644
--- a/include/scratchcpp/scratchconfiguration.h
+++ b/include/scratchcpp/scratchconfiguration.h
@@ -40,6 +40,11 @@ class LIBSCRATCHCPP_EXPORT ScratchConfiguration
         static void removeGraphicsEffect(const std::string &name);
         static IGraphicsEffect *getGraphicsEffect(const std::string &name);
 
+        static const std::string &version();
+        static int majorVersion();
+        static int minorVersion();
+        static int patchVersion();
+
     private:
         static const std::vector<std::shared_ptr<IExtension>> getExtensions();
 
diff --git a/src/scratchconfiguration.cpp b/src/scratchconfiguration.cpp
index 196813bb..24411dec 100644
--- a/src/scratchconfiguration.cpp
+++ b/src/scratchconfiguration.cpp
@@ -49,6 +49,31 @@ IGraphicsEffect *ScratchConfiguration::getGraphicsEffect(const std::string &name
         return it->second.get();
 }
 
+/*! Returns the version string of the library. */
+const std::string &ScratchConfiguration::version()
+{
+    static const std::string ret = LIBSCRATCHCPP_VERSION;
+    return ret;
+}
+
+/*! Returns the major version of the library. */
+int ScratchConfiguration::majorVersion()
+{
+    return LIBSCRATCHCPP_VERSION_MAJOR;
+}
+
+/*! Returns the minor version of the library. */
+int ScratchConfiguration::minorVersion()
+{
+    return LIBSCRATCHCPP_VERSION_MINOR;
+}
+
+/*! Returns the patch version of the library. */
+int ScratchConfiguration::patchVersion()
+{
+    return LIBSCRATCHCPP_VERSION_PATCH;
+}
+
 const std::vector<std::shared_ptr<IExtension>> ScratchConfiguration::getExtensions()
 {
     return impl->extensions;

From 726af1b43679552ab5ccce3189f811ff994608e7 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Thu, 7 Mar 2024 15:34:33 +0100
Subject: [PATCH 14/77] fix #506: Initialize audio when used for the first time

---
 src/audio/audioengine.cpp | 24 +++++++++++++++++-------
 src/audio/audioengine.h   |  2 ++
 2 files changed, 19 insertions(+), 7 deletions(-)

diff --git a/src/audio/audioengine.cpp b/src/audio/audioengine.cpp
index c74e0602..710a394e 100644
--- a/src/audio/audioengine.cpp
+++ b/src/audio/audioengine.cpp
@@ -10,15 +10,31 @@ AudioEngine AudioEngine::instance;
 
 ma_engine *AudioEngine::engine()
 {
-    return &instance.m_engine;
+    if (!instance.m_initialized)
+        instance.init();
+
+    return instance.m_initialized ? &instance.m_engine : nullptr;
 }
 
 bool AudioEngine::initialized()
 {
+    if (!instance.m_initialized)
+        instance.init();
+
     return instance.m_initialized;
 }
 
 AudioEngine::AudioEngine()
+{
+}
+
+AudioEngine::~AudioEngine()
+{
+    if (m_initialized)
+        ma_engine_uninit(&m_engine);
+}
+
+void AudioEngine::init()
 {
     ma_result result;
     result = ma_engine_init(NULL, &m_engine);
@@ -30,9 +46,3 @@ AudioEngine::AudioEngine()
 
     m_initialized = true;
 }
-
-AudioEngine::~AudioEngine()
-{
-    if (m_initialized)
-        ma_engine_uninit(&m_engine);
-}
diff --git a/src/audio/audioengine.h b/src/audio/audioengine.h
index d38e3e94..49097871 100644
--- a/src/audio/audioengine.h
+++ b/src/audio/audioengine.h
@@ -19,6 +19,8 @@ class AudioEngine
         AudioEngine();
         ~AudioEngine();
 
+        void init();
+
         static AudioEngine instance;
         ma_engine m_engine;
         bool m_initialized = false;

From 8476153294e11e17fd26af8b83ed515ff33c014c Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Sun, 10 Mar 2024 16:50:39 +0100
Subject: [PATCH 15/77] Move internal audio source files to internal subdir

---
 src/audio/CMakeLists.txt                           | 14 +++++++-------
 src/audio/audioplayerfactory.cpp                   |  4 ++--
 src/audio/{ => internal}/audioengine.cpp           |  0
 src/audio/{ => internal}/audioengine.h             |  0
 src/audio/{ => internal}/audioplayer.cpp           |  0
 src/audio/{ => internal}/audioplayer.h             |  2 +-
 src/audio/{ => internal}/audioplayerstub.cpp       |  0
 src/audio/{ => internal}/audioplayerstub.h         |  0
 .../thirdparty/miniaudio/miniaudio.c               |  0
 .../thirdparty/miniaudio/miniaudio.h               |  0
 test/audio/audioplayerfactory_test.cpp             |  2 +-
 11 files changed, 11 insertions(+), 11 deletions(-)
 rename src/audio/{ => internal}/audioengine.cpp (100%)
 rename src/audio/{ => internal}/audioengine.h (100%)
 rename src/audio/{ => internal}/audioplayer.cpp (100%)
 rename src/audio/{ => internal}/audioplayer.h (95%)
 rename src/audio/{ => internal}/audioplayerstub.cpp (100%)
 rename src/audio/{ => internal}/audioplayerstub.h (100%)
 rename src/audio/{ => internal}/thirdparty/miniaudio/miniaudio.c (100%)
 rename src/audio/{ => internal}/thirdparty/miniaudio/miniaudio.h (100%)

diff --git a/src/audio/CMakeLists.txt b/src/audio/CMakeLists.txt
index 291d16c5..cf33b0ae 100644
--- a/src/audio/CMakeLists.txt
+++ b/src/audio/CMakeLists.txt
@@ -1,7 +1,7 @@
 option(LIBSCRATCHCPP_AUDIO_SUPPORT "Audio support" ON)
 
 add_library(scratchcpp-audio STATIC)
-set(MINIAUDIO_SRC thirdparty/miniaudio)
+set(MINIAUDIO_SRC internal/thirdparty/miniaudio)
 
 if (LIBSCRATCHCPP_AUDIO_SUPPORT)
     add_library(miniaudio STATIC
@@ -24,15 +24,15 @@ target_sources(scratchcpp-audio
 if (LIBSCRATCHCPP_AUDIO_SUPPORT)
     target_sources(scratchcpp-audio
       PUBLIC
-        audioengine.cpp
-	audioengine.h
-	audioplayer.cpp
-	audioplayer.h
+        internal/audioengine.cpp
+        internal/audioengine.h
+        internal/audioplayer.cpp
+        internal/audioplayer.h
     )
 else()
     target_sources(scratchcpp-audio
       PUBLIC
-        audioplayerstub.cpp
-	audioplayerstub.h
+        internal/audioplayerstub.cpp
+        internal/audioplayerstub.h
     )
 endif()
diff --git a/src/audio/audioplayerfactory.cpp b/src/audio/audioplayerfactory.cpp
index 841af257..7395a0f0 100644
--- a/src/audio/audioplayerfactory.cpp
+++ b/src/audio/audioplayerfactory.cpp
@@ -3,9 +3,9 @@
 #include "audioplayerfactory.h"
 
 #ifdef LIBSCRATCHCPP_AUDIO_SUPPORT
-#include "audioplayer.h"
+#include "internal/audioplayer.h"
 #else
-#include "audioplayerstub.h"
+#include "internal/audioplayerstub.h"
 #endif
 
 using namespace libscratchcpp;
diff --git a/src/audio/audioengine.cpp b/src/audio/internal/audioengine.cpp
similarity index 100%
rename from src/audio/audioengine.cpp
rename to src/audio/internal/audioengine.cpp
diff --git a/src/audio/audioengine.h b/src/audio/internal/audioengine.h
similarity index 100%
rename from src/audio/audioengine.h
rename to src/audio/internal/audioengine.h
diff --git a/src/audio/audioplayer.cpp b/src/audio/internal/audioplayer.cpp
similarity index 100%
rename from src/audio/audioplayer.cpp
rename to src/audio/internal/audioplayer.cpp
diff --git a/src/audio/audioplayer.h b/src/audio/internal/audioplayer.h
similarity index 95%
rename from src/audio/audioplayer.h
rename to src/audio/internal/audioplayer.h
index 04feff50..9cb3661b 100644
--- a/src/audio/audioplayer.h
+++ b/src/audio/internal/audioplayer.h
@@ -2,7 +2,7 @@
 
 #pragma once
 
-#include "iaudioplayer.h"
+#include "../iaudioplayer.h"
 
 struct ma_decoder;
 struct ma_sound;
diff --git a/src/audio/audioplayerstub.cpp b/src/audio/internal/audioplayerstub.cpp
similarity index 100%
rename from src/audio/audioplayerstub.cpp
rename to src/audio/internal/audioplayerstub.cpp
diff --git a/src/audio/audioplayerstub.h b/src/audio/internal/audioplayerstub.h
similarity index 100%
rename from src/audio/audioplayerstub.h
rename to src/audio/internal/audioplayerstub.h
diff --git a/src/audio/thirdparty/miniaudio/miniaudio.c b/src/audio/internal/thirdparty/miniaudio/miniaudio.c
similarity index 100%
rename from src/audio/thirdparty/miniaudio/miniaudio.c
rename to src/audio/internal/thirdparty/miniaudio/miniaudio.c
diff --git a/src/audio/thirdparty/miniaudio/miniaudio.h b/src/audio/internal/thirdparty/miniaudio/miniaudio.h
similarity index 100%
rename from src/audio/thirdparty/miniaudio/miniaudio.h
rename to src/audio/internal/thirdparty/miniaudio/miniaudio.h
diff --git a/test/audio/audioplayerfactory_test.cpp b/test/audio/audioplayerfactory_test.cpp
index 3273fafb..9d02f03b 100644
--- a/test/audio/audioplayerfactory_test.cpp
+++ b/test/audio/audioplayerfactory_test.cpp
@@ -1,5 +1,5 @@
 #include <audio/audioplayerfactory.h>
-#include <audio/audioplayer.h>
+#include <audio/internal/audioplayer.h>
 
 #include "../common.h"
 

From 29233a15bcfdb928f325b758634e0ec53a2b69cb Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Sun, 10 Mar 2024 17:24:32 +0100
Subject: [PATCH 16/77] Replace AudioPlayerFactory with AudioOutput

---
 src/audio/CMakeLists.txt                      |  6 ++--
 ...audioplayerfactory.cpp => audiooutput.cpp} | 10 +++---
 src/audio/audiooutput.h                       | 23 +++++++++++++
 src/audio/audioplayerfactory.h                | 22 ------------
 .../{iaudioplayerfactory.h => iaudiooutput.h} |  6 ++--
 src/scratch/sound.cpp                         |  1 -
 src/scratch/sound_p.cpp                       | 12 +++----
 src/scratch/sound_p.h                         |  4 +--
 test/assets/sound_test.cpp                    | 12 +++----
 test/audio/CMakeLists.txt                     | 10 +++---
 ...rfactory_test.cpp => audiooutput_test.cpp} |  8 ++---
 test/blocks/sound_blocks_test.cpp             | 34 +++++++++----------
 test/engine/engine_test.cpp                   | 10 +++---
 test/mocks/audiooutputmock.h                  | 12 +++++++
 test/mocks/audioplayerfactorymock.h           | 12 -------
 test/scratch_classes/target_test.cpp          | 20 +++++------
 16 files changed, 101 insertions(+), 101 deletions(-)
 rename src/audio/{audioplayerfactory.cpp => audiooutput.cpp} (54%)
 create mode 100644 src/audio/audiooutput.h
 delete mode 100644 src/audio/audioplayerfactory.h
 rename src/audio/{iaudioplayerfactory.h => iaudiooutput.h} (55%)
 rename test/audio/{audioplayerfactory_test.cpp => audiooutput_test.cpp} (50%)
 create mode 100644 test/mocks/audiooutputmock.h
 delete mode 100644 test/mocks/audioplayerfactorymock.h

diff --git a/src/audio/CMakeLists.txt b/src/audio/CMakeLists.txt
index cf33b0ae..db371c6c 100644
--- a/src/audio/CMakeLists.txt
+++ b/src/audio/CMakeLists.txt
@@ -16,9 +16,9 @@ endif()
 target_sources(scratchcpp-audio
   PUBLIC
     iaudioplayer.h
-    iaudioplayerfactory.h
-    audioplayerfactory.cpp
-    audioplayerfactory.h
+    iaudiooutput.h
+    audiooutput.cpp
+    audiooutput.h
 )
 
 if (LIBSCRATCHCPP_AUDIO_SUPPORT)
diff --git a/src/audio/audioplayerfactory.cpp b/src/audio/audiooutput.cpp
similarity index 54%
rename from src/audio/audioplayerfactory.cpp
rename to src/audio/audiooutput.cpp
index 7395a0f0..c7f15d4c 100644
--- a/src/audio/audioplayerfactory.cpp
+++ b/src/audio/audiooutput.cpp
@@ -1,6 +1,6 @@
 // SPDX-License-Identifier: Apache-2.0
 
-#include "audioplayerfactory.h"
+#include "audiooutput.h"
 
 #ifdef LIBSCRATCHCPP_AUDIO_SUPPORT
 #include "internal/audioplayer.h"
@@ -10,18 +10,18 @@
 
 using namespace libscratchcpp;
 
-std::shared_ptr<AudioPlayerFactory> AudioPlayerFactory::m_instance = std::make_shared<AudioPlayerFactory>();
+std::shared_ptr<IAudioOutput> AudioOutput::m_instance = std::make_shared<AudioOutput>();
 
-AudioPlayerFactory::AudioPlayerFactory()
+AudioOutput::AudioOutput()
 {
 }
 
-std::shared_ptr<AudioPlayerFactory> AudioPlayerFactory::instance()
+std::shared_ptr<IAudioOutput> AudioOutput::instance()
 {
     return m_instance;
 }
 
-std::shared_ptr<IAudioPlayer> AudioPlayerFactory::create() const
+std::shared_ptr<IAudioPlayer> AudioOutput::createAudioPlayer() const
 {
 #ifdef LIBSCRATCHCPP_AUDIO_SUPPORT
     return std::make_shared<AudioPlayer>();
diff --git a/src/audio/audiooutput.h b/src/audio/audiooutput.h
new file mode 100644
index 00000000..bb6e734b
--- /dev/null
+++ b/src/audio/audiooutput.h
@@ -0,0 +1,23 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#pragma once
+
+#include "iaudiooutput.h"
+
+namespace libscratchcpp
+{
+
+class AudioOutput : public IAudioOutput
+{
+    public:
+        AudioOutput();
+
+        static std::shared_ptr<IAudioOutput> instance();
+
+        std::shared_ptr<IAudioPlayer> createAudioPlayer() const override;
+
+    private:
+        static std::shared_ptr<IAudioOutput> m_instance;
+};
+
+} // namespace libscratchcpp
diff --git a/src/audio/audioplayerfactory.h b/src/audio/audioplayerfactory.h
deleted file mode 100644
index 33993fae..00000000
--- a/src/audio/audioplayerfactory.h
+++ /dev/null
@@ -1,22 +0,0 @@
-// SPDX-License-Identifier: Apache-2.0
-
-#pragma once
-
-#include "iaudioplayerfactory.h"
-
-namespace libscratchcpp
-{
-
-class AudioPlayerFactory : public IAudioPlayerFactory
-{
-    public:
-        AudioPlayerFactory();
-
-        static std::shared_ptr<AudioPlayerFactory> instance();
-        std::shared_ptr<IAudioPlayer> create() const override;
-
-    private:
-        static std::shared_ptr<AudioPlayerFactory> m_instance;
-};
-
-} // namespace libscratchcpp
diff --git a/src/audio/iaudioplayerfactory.h b/src/audio/iaudiooutput.h
similarity index 55%
rename from src/audio/iaudioplayerfactory.h
rename to src/audio/iaudiooutput.h
index 2792b912..83233990 100644
--- a/src/audio/iaudioplayerfactory.h
+++ b/src/audio/iaudiooutput.h
@@ -9,12 +9,12 @@ namespace libscratchcpp
 
 class IAudioPlayer;
 
-class IAudioPlayerFactory
+class IAudioOutput
 {
     public:
-        virtual ~IAudioPlayerFactory() { }
+        virtual ~IAudioOutput() { }
 
-        virtual std::shared_ptr<IAudioPlayer> create() const = 0;
+        virtual std::shared_ptr<IAudioPlayer> createAudioPlayer() const = 0;
 };
 
 } // namespace libscratchcpp
diff --git a/src/scratch/sound.cpp b/src/scratch/sound.cpp
index 3088ea41..34265722 100644
--- a/src/scratch/sound.cpp
+++ b/src/scratch/sound.cpp
@@ -4,7 +4,6 @@
 #include <iostream>
 
 #include "sound_p.h"
-#include "audio/audioplayerfactory.h"
 
 using namespace libscratchcpp;
 
diff --git a/src/scratch/sound_p.cpp b/src/scratch/sound_p.cpp
index 33fd6b0d..85b354e0 100644
--- a/src/scratch/sound_p.cpp
+++ b/src/scratch/sound_p.cpp
@@ -1,17 +1,17 @@
 // SPDX-License-Identifier: Apache-2.0
 
 #include "sound_p.h"
-#include "audio/audioplayerfactory.h"
+#include "audio/audiooutput.h"
 
 using namespace libscratchcpp;
 
-IAudioPlayerFactory *SoundPrivate::playerFactory = nullptr;
+IAudioOutput *SoundPrivate::audioOutput = nullptr;
 
 SoundPrivate::SoundPrivate()
 {
-    // NOTE: playerFactory must be initialized in the constructor to avoid static initialization order fiasco
-    if (!playerFactory)
-        playerFactory = AudioPlayerFactory::instance().get();
+    // NOTE: audioOutput must be initialized in the constructor to avoid static initialization order fiasco
+    if (!audioOutput)
+        audioOutput = AudioOutput::instance().get();
 
-    player = playerFactory->create();
+    player = audioOutput->createAudioPlayer();
 }
diff --git a/src/scratch/sound_p.h b/src/scratch/sound_p.h
index e979c3f8..082b05fd 100644
--- a/src/scratch/sound_p.h
+++ b/src/scratch/sound_p.h
@@ -4,7 +4,7 @@
 
 #include <string>
 
-#include "audio/iaudioplayerfactory.h"
+#include "audio/iaudiooutput.h"
 #include "audio/iaudioplayer.h"
 
 namespace libscratchcpp
@@ -17,7 +17,7 @@ struct SoundPrivate
 
         int rate = 0;
         int sampleCount = 0;
-        static IAudioPlayerFactory *playerFactory;
+        static IAudioOutput *audioOutput;
         std::shared_ptr<IAudioPlayer> player = nullptr;
 };
 
diff --git a/test/assets/sound_test.cpp b/test/assets/sound_test.cpp
index b37f4763..d35a0215 100644
--- a/test/assets/sound_test.cpp
+++ b/test/assets/sound_test.cpp
@@ -1,6 +1,6 @@
 #include <scratchcpp/sound.h>
 #include <scratch/sound_p.h>
-#include <audioplayerfactorymock.h>
+#include <audiooutputmock.h>
 #include <audioplayermock.h>
 
 #include "../common.h"
@@ -15,13 +15,13 @@ class SoundTest : public testing::Test
         void SetUp() override
         {
             m_player = std::make_shared<AudioPlayerMock>();
-            SoundPrivate::playerFactory = &m_playerFactory;
-            EXPECT_CALL(m_playerFactory, create()).WillOnce(Return(m_player));
+            SoundPrivate::audioOutput = &m_playerFactory;
+            EXPECT_CALL(m_playerFactory, createAudioPlayer()).WillOnce(Return(m_player));
         }
 
-        void TearDown() override { SoundPrivate::playerFactory = nullptr; }
+        void TearDown() override { SoundPrivate::audioOutput = nullptr; }
 
-        AudioPlayerFactoryMock m_playerFactory;
+        AudioOutputMock m_playerFactory;
         std::shared_ptr<AudioPlayerMock> m_player;
 };
 
@@ -98,5 +98,5 @@ TEST_F(SoundTest, IsPlaying)
     EXPECT_CALL(*m_player, isPlaying()).WillOnce(Return(false));
     ASSERT_FALSE(sound.isPlaying());
 
-    SoundPrivate::playerFactory = nullptr;
+    SoundPrivate::audioOutput = nullptr;
 }
diff --git a/test/audio/CMakeLists.txt b/test/audio/CMakeLists.txt
index de2ff75e..e96a0d8b 100644
--- a/test/audio/CMakeLists.txt
+++ b/test/audio/CMakeLists.txt
@@ -1,15 +1,15 @@
 if (LIBSCRATCHCPP_AUDIO_SUPPORT)
-    # audioplayerfactory_test
+    # audiooutput_test
     add_executable(
-      audioplayerfactory_test
-      audioplayerfactory_test.cpp
+      audiooutput_test
+      audiooutput_test.cpp
     )
 
     target_link_libraries(
-      audioplayerfactory_test
+      audiooutput_test
       GTest::gtest_main
       scratchcpp
     )
 
-    gtest_discover_tests(audioplayerfactory_test)
+    gtest_discover_tests(audiooutput_test)
 endif()
diff --git a/test/audio/audioplayerfactory_test.cpp b/test/audio/audiooutput_test.cpp
similarity index 50%
rename from test/audio/audioplayerfactory_test.cpp
rename to test/audio/audiooutput_test.cpp
index 9d02f03b..fde34112 100644
--- a/test/audio/audioplayerfactory_test.cpp
+++ b/test/audio/audiooutput_test.cpp
@@ -1,14 +1,14 @@
-#include <audio/audioplayerfactory.h>
+#include <audio/audiooutput.h>
 #include <audio/internal/audioplayer.h>
 
 #include "../common.h"
 
 using namespace libscratchcpp;
 
-TEST(AudioPlayerFactoryTest, Create)
+TEST(AudioOutputTest, Create)
 {
-    auto factory = AudioPlayerFactory::instance();
-    std::shared_ptr<IAudioPlayer> player = factory->create();
+    auto factory = AudioOutput::instance();
+    std::shared_ptr<IAudioPlayer> player = factory->createAudioPlayer();
     ASSERT_TRUE(player);
     ASSERT_TRUE(std::dynamic_pointer_cast<AudioPlayer>(player));
 }
diff --git a/test/blocks/sound_blocks_test.cpp b/test/blocks/sound_blocks_test.cpp
index 32b813c7..d533faed 100644
--- a/test/blocks/sound_blocks_test.cpp
+++ b/test/blocks/sound_blocks_test.cpp
@@ -6,7 +6,7 @@
 #include <scratchcpp/sound.h>
 #include <scratch/sound_p.h>
 #include <enginemock.h>
-#include <audioplayerfactorymock.h>
+#include <audiooutputmock.h>
 #include <audioplayermock.h>
 
 #include "../common.h"
@@ -117,11 +117,11 @@ TEST_F(SoundBlocksTest, RegisterBlocks)
 
 TEST_F(SoundBlocksTest, Play)
 {
-    AudioPlayerFactoryMock factory;
+    AudioOutputMock factory;
     auto player1 = std::make_shared<AudioPlayerMock>();
     auto player2 = std::make_shared<AudioPlayerMock>();
-    SoundPrivate::playerFactory = &factory;
-    EXPECT_CALL(factory, create()).WillOnce(Return(player1)).WillOnce(Return(player2));
+    SoundPrivate::audioOutput = &factory;
+    EXPECT_CALL(factory, createAudioPlayer()).WillOnce(Return(player1)).WillOnce(Return(player2));
     EXPECT_CALL(*player1, setVolume);
     EXPECT_CALL(*player2, setVolume);
     Target target;
@@ -188,7 +188,7 @@ TEST_F(SoundBlocksTest, Play)
                                     vm::OP_NULL,  vm::OP_EXEC,  3, vm::OP_HALT }));
     ASSERT_EQ(compiler.constValues(), std::vector<Value>({ 1, 0, 4, -4 }));
 
-    SoundPrivate::playerFactory = nullptr;
+    SoundPrivate::audioOutput = nullptr;
 }
 
 TEST_F(SoundBlocksTest, PlayImpl)
@@ -204,12 +204,12 @@ TEST_F(SoundBlocksTest, PlayImpl)
     static BlockFunc functions[] = { &SoundBlocks::playByIndex, &SoundBlocks::play };
     static Value constValues[] = { 2, 5, -1, "test", "nonexistent", "1", "4", "-3" };
 
-    AudioPlayerFactoryMock factory;
+    AudioOutputMock factory;
     auto player1 = std::make_shared<AudioPlayerMock>();
     auto player2 = std::make_shared<AudioPlayerMock>();
     auto player3 = std::make_shared<AudioPlayerMock>();
-    SoundPrivate::playerFactory = &factory;
-    EXPECT_CALL(factory, create()).WillOnce(Return(player1)).WillOnce(Return(player2)).WillOnce(Return(player3));
+    SoundPrivate::audioOutput = &factory;
+    EXPECT_CALL(factory, createAudioPlayer()).WillOnce(Return(player1)).WillOnce(Return(player2)).WillOnce(Return(player3));
     EXPECT_CALL(*player1, setVolume);
     EXPECT_CALL(*player2, setVolume);
     EXPECT_CALL(*player3, setVolume);
@@ -275,16 +275,16 @@ TEST_F(SoundBlocksTest, PlayImpl)
 
     ASSERT_EQ(vm.registerCount(), 0);
 
-    SoundPrivate::playerFactory = nullptr;
+    SoundPrivate::audioOutput = nullptr;
 }
 
 TEST_F(SoundBlocksTest, PlayUntilDone)
 {
-    AudioPlayerFactoryMock factory;
+    AudioOutputMock factory;
     auto player1 = std::make_shared<AudioPlayerMock>();
     auto player2 = std::make_shared<AudioPlayerMock>();
-    SoundPrivate::playerFactory = &factory;
-    EXPECT_CALL(factory, create()).WillOnce(Return(player1)).WillOnce(Return(player2));
+    SoundPrivate::audioOutput = &factory;
+    EXPECT_CALL(factory, createAudioPlayer()).WillOnce(Return(player1)).WillOnce(Return(player2));
     EXPECT_CALL(*player1, setVolume);
     EXPECT_CALL(*player2, setVolume);
     Target target;
@@ -388,7 +388,7 @@ TEST_F(SoundBlocksTest, PlayUntilDone)
               vm::OP_HALT }));
     ASSERT_EQ(compiler.constValues(), std::vector<Value>({ 1, 0, 4, -4 }));
 
-    SoundPrivate::playerFactory = nullptr;
+    SoundPrivate::audioOutput = nullptr;
 }
 
 TEST_F(SoundBlocksTest, PlayUntilDoneImpl)
@@ -404,12 +404,12 @@ TEST_F(SoundBlocksTest, PlayUntilDoneImpl)
     static BlockFunc functions[] = { &SoundBlocks::playByIndexUntilDone, &SoundBlocks::checkSoundByIndex, &SoundBlocks::playUntilDone, &SoundBlocks::checkSound };
     static Value constValues[] = { 2, 5, -1, "test", "nonexistent", "1", "4", "-3" };
 
-    AudioPlayerFactoryMock factory;
+    AudioOutputMock factory;
     auto player1 = std::make_shared<AudioPlayerMock>();
     auto player2 = std::make_shared<AudioPlayerMock>();
     auto player3 = std::make_shared<AudioPlayerMock>();
-    SoundPrivate::playerFactory = &factory;
-    EXPECT_CALL(factory, create()).WillOnce(Return(player1)).WillOnce(Return(player2)).WillOnce(Return(player3));
+    SoundPrivate::audioOutput = &factory;
+    EXPECT_CALL(factory, createAudioPlayer()).WillOnce(Return(player1)).WillOnce(Return(player2)).WillOnce(Return(player3));
     EXPECT_CALL(*player1, setVolume);
     EXPECT_CALL(*player2, setVolume);
     EXPECT_CALL(*player3, setVolume);
@@ -551,7 +551,7 @@ TEST_F(SoundBlocksTest, PlayUntilDoneImpl)
     ASSERT_EQ(vm2.registerCount(), 0);
     ASSERT_TRUE(vm2.atEnd());
 
-    SoundPrivate::playerFactory = nullptr;
+    SoundPrivate::audioOutput = nullptr;
 }
 
 TEST_F(SoundBlocksTest, StopAllSounds)
diff --git a/test/engine/engine_test.cpp b/test/engine/engine_test.cpp
index 0fdc8dc4..7cd6e32a 100644
--- a/test/engine/engine_test.cpp
+++ b/test/engine/engine_test.cpp
@@ -14,7 +14,7 @@
 #include <scratch/sound_p.h>
 #include <timermock.h>
 #include <clockmock.h>
-#include <audioplayerfactorymock.h>
+#include <audiooutputmock.h>
 #include <audioplayermock.h>
 #include <monitorhandlermock.h>
 #include <thread>
@@ -212,12 +212,12 @@ TEST(EngineTest, IsRunning)
 
 TEST(EngineTest, StopSounds)
 {
-    AudioPlayerFactoryMock factory;
+    AudioOutputMock factory;
     auto player1 = std::make_shared<AudioPlayerMock>();
     auto player2 = std::make_shared<AudioPlayerMock>();
     auto player3 = std::make_shared<AudioPlayerMock>();
-    SoundPrivate::playerFactory = &factory;
-    EXPECT_CALL(factory, create()).WillOnce(Return(player1)).WillOnce(Return(player2)).WillOnce(Return(player3));
+    SoundPrivate::audioOutput = &factory;
+    EXPECT_CALL(factory, createAudioPlayer()).WillOnce(Return(player1)).WillOnce(Return(player2)).WillOnce(Return(player3));
     EXPECT_CALL(*player1, load).WillOnce(Return(true));
     EXPECT_CALL(*player2, load).WillOnce(Return(true));
     EXPECT_CALL(*player3, load).WillOnce(Return(true));
@@ -236,7 +236,7 @@ TEST(EngineTest, StopSounds)
     EXPECT_CALL(*player3, stop());
     engine->stopSounds();
 
-    SoundPrivate::playerFactory = nullptr;
+    SoundPrivate::audioOutput = nullptr;
 }
 
 TEST(EngineTest, Step)
diff --git a/test/mocks/audiooutputmock.h b/test/mocks/audiooutputmock.h
new file mode 100644
index 00000000..4b5c79c8
--- /dev/null
+++ b/test/mocks/audiooutputmock.h
@@ -0,0 +1,12 @@
+#pragma once
+
+#include <audio/iaudiooutput.h>
+#include <gmock/gmock.h>
+
+using namespace libscratchcpp;
+
+class AudioOutputMock : public IAudioOutput
+{
+    public:
+        MOCK_METHOD(std::shared_ptr<IAudioPlayer>, createAudioPlayer, (), (const, override));
+};
diff --git a/test/mocks/audioplayerfactorymock.h b/test/mocks/audioplayerfactorymock.h
deleted file mode 100644
index 0f6a81b7..00000000
--- a/test/mocks/audioplayerfactorymock.h
+++ /dev/null
@@ -1,12 +0,0 @@
-#pragma once
-
-#include <audio/iaudioplayerfactory.h>
-#include <gmock/gmock.h>
-
-using namespace libscratchcpp;
-
-class AudioPlayerFactoryMock : public IAudioPlayerFactory
-{
-    public:
-        MOCK_METHOD(std::shared_ptr<IAudioPlayer>, create, (), (const, override));
-};
diff --git a/test/scratch_classes/target_test.cpp b/test/scratch_classes/target_test.cpp
index c0e2ffec..6924980d 100644
--- a/test/scratch_classes/target_test.cpp
+++ b/test/scratch_classes/target_test.cpp
@@ -9,7 +9,7 @@
 #include <enginemock.h>
 #include <targetmock.h>
 #include <graphicseffectmock.h>
-#include <audioplayerfactorymock.h>
+#include <audiooutputmock.h>
 #include <audioplayermock.h>
 
 #include "../common.h"
@@ -406,12 +406,12 @@ TEST(TargetTest, Costumes)
 
 TEST(TargetTest, Sounds)
 {
-    AudioPlayerFactoryMock factory;
+    AudioOutputMock factory;
     auto player1 = std::make_shared<AudioPlayerMock>();
     auto player2 = std::make_shared<AudioPlayerMock>();
     auto player3 = std::make_shared<AudioPlayerMock>();
-    SoundPrivate::playerFactory = &factory;
-    EXPECT_CALL(factory, create()).WillOnce(Return(player1)).WillOnce(Return(player2)).WillOnce(Return(player3));
+    SoundPrivate::audioOutput = &factory;
+    EXPECT_CALL(factory, createAudioPlayer()).WillOnce(Return(player1)).WillOnce(Return(player2)).WillOnce(Return(player3));
     auto s1 = std::make_shared<Sound>("sound1", "", "mp3");
     auto s2 = std::make_shared<Sound>("sound2", "", "wav");
     auto s3 = std::make_shared<Sound>("sound3", "", "mp3");
@@ -472,7 +472,7 @@ TEST(TargetTest, Sounds)
     ASSERT_EQ(target2.sounds(), source.sounds());
 
     auto player4 = std::make_shared<AudioPlayerMock>();
-    EXPECT_CALL(factory, create()).WillOnce(Return(player4));
+    EXPECT_CALL(factory, createAudioPlayer()).WillOnce(Return(player4));
     auto s4 = std::make_shared<Sound>("sound4", "", "wav");
 
     EXPECT_CALL(*player4, setVolume(1));
@@ -495,7 +495,7 @@ TEST(TargetTest, Sounds)
     ASSERT_EQ(source.findSound("sound3"), 2);
     ASSERT_EQ(source.findSound("sound4"), 3);
 
-    SoundPrivate::playerFactory = nullptr;
+    SoundPrivate::audioOutput = nullptr;
 }
 
 TEST(TargetTest, LayerOrder)
@@ -523,12 +523,12 @@ TEST(TargetTest, Volume)
     target.setVolume(64.9);
     ASSERT_EQ(target.volume(), 64.9);
 
-    AudioPlayerFactoryMock factory;
+    AudioOutputMock factory;
     auto player1 = std::make_shared<AudioPlayerMock>();
     auto player2 = std::make_shared<AudioPlayerMock>();
     auto player3 = std::make_shared<AudioPlayerMock>();
-    SoundPrivate::playerFactory = &factory;
-    EXPECT_CALL(factory, create()).WillOnce(Return(player1)).WillOnce(Return(player2)).WillOnce(Return(player3));
+    SoundPrivate::audioOutput = &factory;
+    EXPECT_CALL(factory, createAudioPlayer()).WillOnce(Return(player1)).WillOnce(Return(player2)).WillOnce(Return(player3));
     auto s1 = std::make_shared<Sound>("", "", "");
     auto s2 = std::make_shared<Sound>("", "", "");
     auto s3 = std::make_shared<Sound>("", "", "");
@@ -557,7 +557,7 @@ TEST(TargetTest, Volume)
     EXPECT_CALL(*player3, setVolume(0));
     target.setVolume(-0.5);
 
-    SoundPrivate::playerFactory = nullptr;
+    SoundPrivate::audioOutput = nullptr;
 }
 
 TEST(TargetTest, GraphicsEffects)

From a821878033c840305c8da1f2e2514d3250b2ecf0 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Sun, 10 Mar 2024 17:48:23 +0100
Subject: [PATCH 17/77] Allow testing AudioOutput without audio support

---
 test/audio/CMakeLists.txt       | 26 ++++++++++++++------------
 test/audio/audiooutput_test.cpp | 10 ++++++++++
 2 files changed, 24 insertions(+), 12 deletions(-)

diff --git a/test/audio/CMakeLists.txt b/test/audio/CMakeLists.txt
index e96a0d8b..93901deb 100644
--- a/test/audio/CMakeLists.txt
+++ b/test/audio/CMakeLists.txt
@@ -1,15 +1,17 @@
-if (LIBSCRATCHCPP_AUDIO_SUPPORT)
-    # audiooutput_test
-    add_executable(
-      audiooutput_test
-      audiooutput_test.cpp
-    )
+# audiooutput_test
+add_executable(
+  audiooutput_test
+  audiooutput_test.cpp
+)
+
+target_link_libraries(
+  audiooutput_test
+  GTest::gtest_main
+  scratchcpp
+)
 
-    target_link_libraries(
-      audiooutput_test
-      GTest::gtest_main
-      scratchcpp
-    )
+gtest_discover_tests(audiooutput_test)
 
-    gtest_discover_tests(audiooutput_test)
+if (LIBSCRATCHCPP_AUDIO_SUPPORT)
+    target_compile_definitions(audiooutput_test PRIVATE LIBSCRATCHCPP_AUDIO_SUPPORT)
 endif()
diff --git a/test/audio/audiooutput_test.cpp b/test/audio/audiooutput_test.cpp
index fde34112..ac3d5451 100644
--- a/test/audio/audiooutput_test.cpp
+++ b/test/audio/audiooutput_test.cpp
@@ -1,5 +1,10 @@
 #include <audio/audiooutput.h>
+
+#ifdef LIBSCRATCHCPP_AUDIO_SUPPORT
 #include <audio/internal/audioplayer.h>
+#else
+#include <audio/internal/audioplayerstub.h>
+#endif
 
 #include "../common.h"
 
@@ -10,5 +15,10 @@ TEST(AudioOutputTest, Create)
     auto factory = AudioOutput::instance();
     std::shared_ptr<IAudioPlayer> player = factory->createAudioPlayer();
     ASSERT_TRUE(player);
+
+#ifdef LIBSCRATCHCPP_AUDIO_SUPPORT
     ASSERT_TRUE(std::dynamic_pointer_cast<AudioPlayer>(player));
+#else
+    ASSERT_TRUE(std::dynamic_pointer_cast<AudioPlayerStub>(player));
+#endif
 }

From 518fa7082020bc94e138e620bb2e9766da4074bc Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Sun, 10 Mar 2024 22:13:07 +0100
Subject: [PATCH 18/77] Add AudioLoudness class

---
 src/audio/CMakeLists.txt                 |  5 ++
 src/audio/iaudioloudness.h               | 16 +++++
 src/audio/internal/audioloudness.cpp     | 86 ++++++++++++++++++++++++
 src/audio/internal/audioloudness.h       | 24 +++++++
 src/audio/internal/audioloudnessstub.cpp | 14 ++++
 src/audio/internal/audioloudnessstub.h   | 18 +++++
 6 files changed, 163 insertions(+)
 create mode 100644 src/audio/iaudioloudness.h
 create mode 100644 src/audio/internal/audioloudness.cpp
 create mode 100644 src/audio/internal/audioloudness.h
 create mode 100644 src/audio/internal/audioloudnessstub.cpp
 create mode 100644 src/audio/internal/audioloudnessstub.h

diff --git a/src/audio/CMakeLists.txt b/src/audio/CMakeLists.txt
index db371c6c..5e5374a3 100644
--- a/src/audio/CMakeLists.txt
+++ b/src/audio/CMakeLists.txt
@@ -19,6 +19,7 @@ target_sources(scratchcpp-audio
     iaudiooutput.h
     audiooutput.cpp
     audiooutput.h
+    iaudioloudness.h
 )
 
 if (LIBSCRATCHCPP_AUDIO_SUPPORT)
@@ -28,11 +29,15 @@ if (LIBSCRATCHCPP_AUDIO_SUPPORT)
         internal/audioengine.h
         internal/audioplayer.cpp
         internal/audioplayer.h
+        internal/audioloudness.cpp
+        internal/audioloudness.h
     )
 else()
     target_sources(scratchcpp-audio
       PUBLIC
         internal/audioplayerstub.cpp
         internal/audioplayerstub.h
+        internal/audioloudnessstub.cpp
+        internal/audioloudnessstub.h
     )
 endif()
diff --git a/src/audio/iaudioloudness.h b/src/audio/iaudioloudness.h
new file mode 100644
index 00000000..3b4488bf
--- /dev/null
+++ b/src/audio/iaudioloudness.h
@@ -0,0 +1,16 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#pragma once
+
+namespace libscratchcpp
+{
+
+class IAudioLoudness
+{
+    public:
+        virtual ~IAudioLoudness() { }
+
+        virtual int getLoudness() const = 0;
+};
+
+} // namespace libscratchcpp
diff --git a/src/audio/internal/audioloudness.cpp b/src/audio/internal/audioloudness.cpp
new file mode 100644
index 00000000..f284d93b
--- /dev/null
+++ b/src/audio/internal/audioloudness.cpp
@@ -0,0 +1,86 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#include <miniaudio.h>
+#include <cmath>
+#include <iostream>
+
+#include "audioloudness.h"
+
+using namespace libscratchcpp;
+
+static int loudness = -1;
+static float lastValue = 0.0f;
+
+static void data_callback(ma_device *pDevice, void *pOutput, const void *pInput, ma_uint32 frameCount)
+{
+    const float *pMicDataArray = static_cast<const float *>(pInput);
+
+    // https://github.com/scratchfoundation/scratch-audio/blob/068aca613604e39b2adbe785b17931cc43eec35f/src/Loudness.js#L36-L80
+    // Compute the RMS of the sound
+    float sum = 0.0f;
+
+    for (ma_uint32 i = 0; i < frameCount; i++) {
+        float value = pMicDataArray[i];
+        sum += value * value;
+    }
+
+    float rms = std::sqrt(sum / static_cast<float>(frameCount));
+
+    // Smooth the value, if it is descending
+    if (lastValue != 0.0f) {
+        rms = std::max(rms, lastValue * 0.6f);
+    }
+
+    lastValue = rms;
+
+    // Scale the measurement
+    rms *= 1.63f;
+    rms = std::sqrt(rms);
+    // Scale it up to 0-100 and round
+    rms = std::round(rms * 100.0f);
+    // Prevent it from going above 100
+    rms = std::min(rms, 100.0f);
+
+    loudness = rms;
+}
+
+AudioLoudness::AudioLoudness()
+{
+    ma_device_config deviceConfig = ma_device_config_init(ma_device_type_capture);
+    deviceConfig.capture.format = ma_format_f32;
+    deviceConfig.capture.channels = 1; // mono
+    deviceConfig.sampleRate = 44100;
+    deviceConfig.periodSizeInFrames = 2048;
+    deviceConfig.dataCallback = data_callback;
+    deviceConfig.pUserData = NULL;
+
+    m_device = new ma_device;
+
+    if (ma_device_init(NULL, &deviceConfig, m_device) != MA_SUCCESS) {
+        std::cerr << "Failed to initialize audio capture device." << std::endl;
+        delete m_device;
+        m_device = nullptr;
+        return;
+    }
+
+    if (ma_device_start(m_device) != MA_SUCCESS) {
+        std::cerr << "Failed to start audio capture device." << std::endl;
+        ma_device_uninit(m_device);
+        delete m_device;
+        m_device = nullptr;
+        return;
+    }
+}
+
+AudioLoudness::~AudioLoudness()
+{
+    if (m_device) {
+        ma_device_uninit(m_device);
+        delete m_device;
+    }
+}
+
+int AudioLoudness::getLoudness() const
+{
+    return loudness;
+}
diff --git a/src/audio/internal/audioloudness.h b/src/audio/internal/audioloudness.h
new file mode 100644
index 00000000..ea0a71b5
--- /dev/null
+++ b/src/audio/internal/audioloudness.h
@@ -0,0 +1,24 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#pragma once
+
+#include "../iaudioloudness.h"
+
+struct ma_device;
+
+namespace libscratchcpp
+{
+
+class AudioLoudness : public IAudioLoudness
+{
+    public:
+        AudioLoudness();
+        ~AudioLoudness();
+
+        int getLoudness() const override;
+
+    private:
+        ma_device *m_device = nullptr;
+};
+
+} // namespace libscratchcpp
diff --git a/src/audio/internal/audioloudnessstub.cpp b/src/audio/internal/audioloudnessstub.cpp
new file mode 100644
index 00000000..9949a6f4
--- /dev/null
+++ b/src/audio/internal/audioloudnessstub.cpp
@@ -0,0 +1,14 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#include "audioloudness.h"
+
+using namespace libscratchcpp;
+
+AudioLoudness::AudioLoudness()
+{
+}
+
+int AudioLoudness::getLoudness() const
+{
+    return -1;
+}
diff --git a/src/audio/internal/audioloudnessstub.h b/src/audio/internal/audioloudnessstub.h
new file mode 100644
index 00000000..bd1be9fa
--- /dev/null
+++ b/src/audio/internal/audioloudnessstub.h
@@ -0,0 +1,18 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#pragma once
+
+#include "../iaudioloudness.h"
+
+namespace libscratchcpp
+{
+
+class AudioLoudness : public IAudioLoudness
+{
+    public:
+        AudioLoudness();
+
+        int getLoudness() const override;
+};
+
+} // namespace libscratchcpp

From 512013ddeec56eeb6f7b670338065a039698c428 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Sun, 10 Mar 2024 22:13:25 +0100
Subject: [PATCH 19/77] Add AudioInput class

---
 src/audio/CMakeLists.txt       |  3 +++
 src/audio/audioinput.cpp       | 35 ++++++++++++++++++++++++++++++++++
 src/audio/audioinput.h         | 27 ++++++++++++++++++++++++++
 src/audio/iaudioinput.h        | 21 ++++++++++++++++++++
 test/audio/CMakeLists.txt      | 18 +++++++++++++++++
 test/audio/audioinput_test.cpp | 24 +++++++++++++++++++++++
 6 files changed, 128 insertions(+)
 create mode 100644 src/audio/audioinput.cpp
 create mode 100644 src/audio/audioinput.h
 create mode 100644 src/audio/iaudioinput.h
 create mode 100644 test/audio/audioinput_test.cpp

diff --git a/src/audio/CMakeLists.txt b/src/audio/CMakeLists.txt
index 5e5374a3..5bf4e0b3 100644
--- a/src/audio/CMakeLists.txt
+++ b/src/audio/CMakeLists.txt
@@ -19,6 +19,9 @@ target_sources(scratchcpp-audio
     iaudiooutput.h
     audiooutput.cpp
     audiooutput.h
+    iaudioinput.h
+    audioinput.h
+    audioinput.cpp
     iaudioloudness.h
 )
 
diff --git a/src/audio/audioinput.cpp b/src/audio/audioinput.cpp
new file mode 100644
index 00000000..38f1c263
--- /dev/null
+++ b/src/audio/audioinput.cpp
@@ -0,0 +1,35 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#include "audioinput.h"
+
+#ifdef LIBSCRATCHCPP_AUDIO_SUPPORT
+#include "internal/audioloudness.h"
+#else
+#include "internal/audioloudnessstub.h"
+#endif
+
+using namespace libscratchcpp;
+
+std::shared_ptr<IAudioInput> AudioInput::m_instance = std::make_shared<AudioInput>();
+
+AudioInput::AudioInput()
+{
+}
+
+std::shared_ptr<IAudioInput> AudioInput::instance()
+{
+    return m_instance;
+}
+
+std::shared_ptr<IAudioLoudness> AudioInput::audioLoudness() const
+{
+    if (!m_audioLoudness) {
+#ifdef LIBSCRATCHCPP_AUDIO_SUPPORT
+        m_audioLoudness = std::make_shared<AudioLoudness>();
+#else
+        m_audioLoudness = std::make_shared<AudioLoudnessStub>();
+#endif
+    }
+
+    return m_audioLoudness;
+}
diff --git a/src/audio/audioinput.h b/src/audio/audioinput.h
new file mode 100644
index 00000000..527dc662
--- /dev/null
+++ b/src/audio/audioinput.h
@@ -0,0 +1,27 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#pragma once
+
+#include <memory>
+
+#include "iaudioinput.h"
+
+namespace libscratchcpp
+{
+
+class AudioInput : public IAudioInput
+{
+    public:
+        AudioInput();
+
+        static std::shared_ptr<IAudioInput> instance();
+
+        std::shared_ptr<IAudioLoudness> audioLoudness() const override;
+
+    private:
+        static std::shared_ptr<IAudioInput> m_instance;
+
+        mutable std::shared_ptr<IAudioLoudness> m_audioLoudness;
+};
+
+} // namespace libscratchcpp
diff --git a/src/audio/iaudioinput.h b/src/audio/iaudioinput.h
new file mode 100644
index 00000000..f26e1441
--- /dev/null
+++ b/src/audio/iaudioinput.h
@@ -0,0 +1,21 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#pragma once
+
+#include <memory>
+
+namespace libscratchcpp
+{
+
+class IAudioLoudness;
+
+class IAudioInput
+{
+
+    public:
+        virtual ~IAudioInput() { }
+
+        virtual std::shared_ptr<IAudioLoudness> audioLoudness() const = 0;
+};
+
+} // namespace libscratchcpp
diff --git a/test/audio/CMakeLists.txt b/test/audio/CMakeLists.txt
index 93901deb..c6a397b6 100644
--- a/test/audio/CMakeLists.txt
+++ b/test/audio/CMakeLists.txt
@@ -15,3 +15,21 @@ gtest_discover_tests(audiooutput_test)
 if (LIBSCRATCHCPP_AUDIO_SUPPORT)
     target_compile_definitions(audiooutput_test PRIVATE LIBSCRATCHCPP_AUDIO_SUPPORT)
 endif()
+
+# audioinput_test
+add_executable(
+  audioinput_test
+  audioinput_test.cpp
+)
+
+target_link_libraries(
+  audioinput_test
+  GTest::gtest_main
+  scratchcpp
+)
+
+gtest_discover_tests(audioinput_test)
+
+if (LIBSCRATCHCPP_AUDIO_SUPPORT)
+    target_compile_definitions(audioinput_test PRIVATE LIBSCRATCHCPP_AUDIO_SUPPORT)
+endif()
diff --git a/test/audio/audioinput_test.cpp b/test/audio/audioinput_test.cpp
new file mode 100644
index 00000000..6162b41e
--- /dev/null
+++ b/test/audio/audioinput_test.cpp
@@ -0,0 +1,24 @@
+#include <audio/audioinput.h>
+
+#ifdef LIBSCRATCHCPP_AUDIO_SUPPORT
+#include <audio/internal/audioloudness.h>
+#else
+#include <audio/internal/audioloudnessstub.h>
+#endif
+
+#include "../common.h"
+
+using namespace libscratchcpp;
+
+TEST(AudioInputTest, Create)
+{
+    auto factory = AudioInput::instance();
+    std::shared_ptr<IAudioLoudness> audioLoudness = factory->audioLoudness();
+    ASSERT_TRUE(audioLoudness);
+
+#ifdef LIBSCRATCHCPP_AUDIO_SUPPORT
+    ASSERT_TRUE(std::dynamic_pointer_cast<AudioLoudness>(audioLoudness));
+#else
+    ASSERT_TRUE(std::dynamic_pointer_cast<AudioLoudnessStub>(audioLoudness));
+#endif
+}

From e30d0c319159f49fa221d4225c207cba9277e37a Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Mon, 11 Mar 2024 19:09:07 +0100
Subject: [PATCH 20/77] AudioLoudness: Make the loudness variable atomic

it's accessed from 2 threads
---
 src/audio/internal/audioloudness.cpp | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/audio/internal/audioloudness.cpp b/src/audio/internal/audioloudness.cpp
index f284d93b..b098a092 100644
--- a/src/audio/internal/audioloudness.cpp
+++ b/src/audio/internal/audioloudness.cpp
@@ -1,6 +1,7 @@
 // SPDX-License-Identifier: Apache-2.0
 
 #include <miniaudio.h>
+#include <atomic>
 #include <cmath>
 #include <iostream>
 
@@ -8,7 +9,7 @@
 
 using namespace libscratchcpp;
 
-static int loudness = -1;
+static std::atomic<int> loudness = -1;
 static float lastValue = 0.0f;
 
 static void data_callback(ma_device *pDevice, void *pOutput, const void *pInput, ma_uint32 frameCount)

From 102c1910814fbfcb5da1373e28b65c70401816e9 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Mon, 11 Mar 2024 19:11:13 +0100
Subject: [PATCH 21/77] AudioLoudness: Adjust the value to be closer to Scratch

---
 src/audio/internal/audioloudness.cpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/audio/internal/audioloudness.cpp b/src/audio/internal/audioloudness.cpp
index b098a092..026115e8 100644
--- a/src/audio/internal/audioloudness.cpp
+++ b/src/audio/internal/audioloudness.cpp
@@ -22,7 +22,7 @@ static void data_callback(ma_device *pDevice, void *pOutput, const void *pInput,
 
     for (ma_uint32 i = 0; i < frameCount; i++) {
         float value = pMicDataArray[i];
-        sum += value * value;
+        sum += std::pow(value, 2) / 20.0f; // TODO: Convert the value properly (it's different than in JS)
     }
 
     float rms = std::sqrt(sum / static_cast<float>(frameCount));

From 9f03569c95ead24326a19f6c76664d1a21d23f34 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Mon, 11 Mar 2024 19:22:22 +0100
Subject: [PATCH 22/77] Add AudioInputMock

---
 test/mocks/audioinputmock.h | 12 ++++++++++++
 1 file changed, 12 insertions(+)
 create mode 100644 test/mocks/audioinputmock.h

diff --git a/test/mocks/audioinputmock.h b/test/mocks/audioinputmock.h
new file mode 100644
index 00000000..31c5ad22
--- /dev/null
+++ b/test/mocks/audioinputmock.h
@@ -0,0 +1,12 @@
+#pragma once
+
+#include <audio/iaudioinput.h>
+#include <gmock/gmock.h>
+
+using namespace libscratchcpp;
+
+class AudioInputMock : public IAudioInput
+{
+    public:
+        MOCK_METHOD(std::shared_ptr<IAudioLoudness>, audioLoudness, (), (const, override));
+};

From 4993b4ec2bd519ade42fbbab148bc7bfd0f487ed Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Mon, 11 Mar 2024 19:22:29 +0100
Subject: [PATCH 23/77] Add AudioLoudnessMock

---
 test/mocks/audioloudnessmock.h | 12 ++++++++++++
 1 file changed, 12 insertions(+)
 create mode 100644 test/mocks/audioloudnessmock.h

diff --git a/test/mocks/audioloudnessmock.h b/test/mocks/audioloudnessmock.h
new file mode 100644
index 00000000..fc7a7107
--- /dev/null
+++ b/test/mocks/audioloudnessmock.h
@@ -0,0 +1,12 @@
+#pragma once
+
+#include <audio/iaudioloudness.h>
+#include <gmock/gmock.h>
+
+using namespace libscratchcpp;
+
+class AudioLoudnessMock : public IAudioLoudness
+{
+    public:
+        MOCK_METHOD(int, getLoudness, (), (const, override));
+};

From 0c527439ad79760a9263ec19ee7bfef791db8766 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Mon, 11 Mar 2024 19:22:55 +0100
Subject: [PATCH 24/77] Implement sensing_loudness block

---
 src/blocks/sensingblocks.cpp        | 26 ++++++++++++
 src/blocks/sensingblocks.h          |  6 +++
 test/blocks/sensing_blocks_test.cpp | 63 +++++++++++++++++++++++++++++
 3 files changed, 95 insertions(+)

diff --git a/src/blocks/sensingblocks.cpp b/src/blocks/sensingblocks.cpp
index 869d7657..40ddcf0e 100644
--- a/src/blocks/sensingblocks.cpp
+++ b/src/blocks/sensingblocks.cpp
@@ -14,10 +14,13 @@
 #include "sensingblocks.h"
 
 #include "../engine/internal/clock.h"
+#include "audio/audioinput.h"
+#include "audio/iaudioloudness.h"
 
 using namespace libscratchcpp;
 
 IClock *SensingBlocks::clock = nullptr;
+IAudioInput *SensingBlocks::audioInput = nullptr;
 
 std::string SensingBlocks::name() const
 {
@@ -35,6 +38,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);
     engine->addCompileFunction(this, "sensing_timer", &compileTimer);
     engine->addCompileFunction(this, "sensing_resettimer", &compileResetTimer);
     engine->addCompileFunction(this, "sensing_of", &compileOf);
@@ -45,6 +49,7 @@ void SensingBlocks::registerBlocks(IEngine *engine)
     engine->addMonitorNameFunction(this, "sensing_mousedown", &mouseDownMonitorName);
     engine->addMonitorNameFunction(this, "sensing_mousex", &mouseXMonitorName);
     engine->addMonitorNameFunction(this, "sensing_mousey", &mouseYMonitorName);
+    engine->addMonitorNameFunction(this, "sensing_loudness", &loudnessMonitorName);
     engine->addMonitorNameFunction(this, "sensing_timer", &timerMonitorName);
     engine->addMonitorNameFunction(this, "sensing_current", &currentMonitorName);
     engine->addMonitorNameFunction(this, "sensing_dayssince2000", &daysSince2000MonitorName);
@@ -156,6 +161,11 @@ void SensingBlocks::compileSetDragMode(Compiler *compiler)
     }
 }
 
+void SensingBlocks::compileLoudness(Compiler *compiler)
+{
+    compiler->addFunctionCall(&loudness);
+}
+
 void SensingBlocks::compileTimer(Compiler *compiler)
 {
     compiler->addFunctionCall(&timer);
@@ -354,6 +364,12 @@ const std::string &SensingBlocks::mouseYMonitorName(Block *block)
     return name;
 }
 
+const std::string &SensingBlocks::loudnessMonitorName(Block *block)
+{
+    static const std::string name = "loudness";
+    return name;
+}
+
 const std::string &SensingBlocks::timerMonitorName(Block *block)
 {
     static const std::string name = "timer";
@@ -453,6 +469,16 @@ unsigned int SensingBlocks::setNotDraggableMode(VirtualMachine *vm)
     return 0;
 }
 
+unsigned int SensingBlocks::loudness(VirtualMachine *vm)
+{
+    if (!audioInput)
+        audioInput = AudioInput::instance().get();
+
+    auto audioLoudness = audioInput->audioLoudness();
+    vm->addReturnValue(audioLoudness->getLoudness());
+    return 0;
+}
+
 unsigned int SensingBlocks::distanceTo(VirtualMachine *vm)
 {
     Sprite *sprite = dynamic_cast<Sprite *>(vm->target());
diff --git a/src/blocks/sensingblocks.h b/src/blocks/sensingblocks.h
index 01d8a608..62bdc6c1 100644
--- a/src/blocks/sensingblocks.h
+++ b/src/blocks/sensingblocks.h
@@ -12,6 +12,7 @@ namespace libscratchcpp
 {
 
 class Target;
+class IAudioInput;
 
 /*! \brief The SensingBlocks class contains the implementation of sensing blocks. */
 class SensingBlocks : public IBlockSection
@@ -66,6 +67,7 @@ class SensingBlocks : public IBlockSection
         static void compileMouseX(Compiler *compiler);
         static void compileMouseY(Compiler *compiler);
         static void compileSetDragMode(Compiler *compiler);
+        static void compileLoudness(Compiler *compiler);
         static void compileTimer(Compiler *compiler);
         static void compileResetTimer(Compiler *compiler);
         static void compileOf(Compiler *compiler);
@@ -75,6 +77,7 @@ class SensingBlocks : public IBlockSection
         static const std::string &mouseDownMonitorName(Block *block);
         static const std::string &mouseXMonitorName(Block *block);
         static const std::string &mouseYMonitorName(Block *block);
+        static const std::string &loudnessMonitorName(Block *block);
         static const std::string &timerMonitorName(Block *block);
         static const std::string &currentMonitorName(Block *block);
         static const std::string &daysSince2000MonitorName(Block *block);
@@ -87,6 +90,8 @@ class SensingBlocks : public IBlockSection
         static unsigned int setDraggableMode(VirtualMachine *vm);
         static unsigned int setNotDraggableMode(VirtualMachine *vm);
 
+        static unsigned int loudness(VirtualMachine *vm);
+
         static unsigned int distanceTo(VirtualMachine *vm);
         static unsigned int distanceToByIndex(VirtualMachine *vm);
         static unsigned int distanceToMousePointer(VirtualMachine *vm);
@@ -128,6 +133,7 @@ class SensingBlocks : public IBlockSection
         static unsigned int daysSince2000(VirtualMachine *vm);
 
         static IClock *clock;
+        static IAudioInput *audioInput;
 
     private:
         struct Question
diff --git a/test/blocks/sensing_blocks_test.cpp b/test/blocks/sensing_blocks_test.cpp
index c287ba4b..e9797eb6 100644
--- a/test/blocks/sensing_blocks_test.cpp
+++ b/test/blocks/sensing_blocks_test.cpp
@@ -9,6 +9,8 @@
 #include <enginemock.h>
 #include <timermock.h>
 #include <clockmock.h>
+#include <audioinputmock.h>
+#include <audioloudnessmock.h>
 
 #include "../common.h"
 #include "blocks/sensingblocks.h"
@@ -129,6 +131,7 @@ TEST_F(SensingBlocksTest, RegisterBlocks)
     EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "sensing_mousex", &SensingBlocks::compileMouseX));
     EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "sensing_mousey", &SensingBlocks::compileMouseY));
     EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "sensing_setdragmode", &SensingBlocks::compileSetDragMode));
+    EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "sensing_loudness", &SensingBlocks::compileLoudness));
     EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "sensing_timer", &SensingBlocks::compileTimer));
     EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "sensing_resettimer", &SensingBlocks::compileResetTimer));
     EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "sensing_of", &SensingBlocks::compileOf));
@@ -139,6 +142,7 @@ TEST_F(SensingBlocksTest, RegisterBlocks)
     EXPECT_CALL(m_engineMock, addMonitorNameFunction(m_section.get(), "sensing_mousedown", &SensingBlocks::mouseDownMonitorName));
     EXPECT_CALL(m_engineMock, addMonitorNameFunction(m_section.get(), "sensing_mousex", &SensingBlocks::mouseXMonitorName));
     EXPECT_CALL(m_engineMock, addMonitorNameFunction(m_section.get(), "sensing_mousey", &SensingBlocks::mouseYMonitorName));
+    EXPECT_CALL(m_engineMock, addMonitorNameFunction(m_section.get(), "sensing_loudness", &SensingBlocks::loudnessMonitorName));
     EXPECT_CALL(m_engineMock, addMonitorNameFunction(m_section.get(), "sensing_timer", &SensingBlocks::timerMonitorName));
     EXPECT_CALL(m_engineMock, addMonitorNameFunction(m_section.get(), "sensing_current", &SensingBlocks::currentMonitorName));
     EXPECT_CALL(m_engineMock, addMonitorNameFunction(m_section.get(), "sensing_dayssince2000", &SensingBlocks::daysSince2000MonitorName));
@@ -741,6 +745,65 @@ TEST_F(SensingBlocksTest, SetDragModeImpl)
     ASSERT_FALSE(sprite.draggable());
 }
 
+TEST_F(SensingBlocksTest, Loudness)
+{
+    Compiler compiler(&m_engineMock);
+
+    auto block = std::make_shared<Block>("a", "sensing_loudness");
+
+    EXPECT_CALL(m_engineMock, functionIndex(&SensingBlocks::loudness)).WillOnce(Return(0));
+    compiler.init();
+
+    compiler.setBlock(block);
+    SensingBlocks::compileLoudness(&compiler);
+
+    compiler.end();
+
+    ASSERT_EQ(compiler.bytecode(), std::vector<unsigned int>({ vm::OP_START, vm::OP_EXEC, 0, vm::OP_HALT }));
+    ASSERT_TRUE(compiler.constValues().empty());
+}
+
+TEST_F(SensingBlocksTest, LoudnessMonitorName)
+{
+    ASSERT_EQ(SensingBlocks::loudnessMonitorName(nullptr), "loudness");
+}
+
+TEST_F(SensingBlocksTest, LoudnessImpl)
+{
+    static unsigned int bytecode[] = { vm::OP_START, vm::OP_EXEC, 0, vm::OP_HALT };
+    static BlockFunc functions[] = { &SensingBlocks::loudness };
+
+    VirtualMachine vm(nullptr, &m_engineMock, nullptr);
+    vm.setFunctions(functions);
+
+    vm.setBytecode(bytecode);
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 1);
+
+    AudioInputMock audioInput;
+    auto audioLoudness = std::make_shared<AudioLoudnessMock>();
+    SensingBlocks::audioInput = &audioInput;
+
+    EXPECT_CALL(audioInput, audioLoudness()).WillOnce(Return(audioLoudness));
+    EXPECT_CALL(*audioLoudness, getLoudness()).WillOnce(Return(-1));
+    vm.reset();
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 1);
+    ASSERT_EQ(vm.getInput(0, 1)->toDouble(), -1);
+
+    EXPECT_CALL(audioInput, audioLoudness()).WillOnce(Return(audioLoudness));
+    EXPECT_CALL(*audioLoudness, getLoudness()).WillOnce(Return(62));
+    vm.reset();
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 1);
+    ASSERT_EQ(vm.getInput(0, 1)->toDouble(), 62);
+
+    SensingBlocks::audioInput = nullptr;
+}
+
 TEST_F(SensingBlocksTest, Timer)
 {
     Compiler compiler(&m_engineMock);

From 746f5084a9fad06ee71269edb375926dc7df6dec Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Mon, 11 Mar 2024 19:47:27 +0100
Subject: [PATCH 25/77] Implement sensing_loud block

---
 src/blocks/sensingblocks.cpp        | 16 ++++++
 src/blocks/sensingblocks.h          |  2 +
 test/blocks/sensing_blocks_test.cpp | 79 +++++++++++++++++++++++++++++
 3 files changed, 97 insertions(+)

diff --git a/src/blocks/sensingblocks.cpp b/src/blocks/sensingblocks.cpp
index 40ddcf0e..180da8b6 100644
--- a/src/blocks/sensingblocks.cpp
+++ b/src/blocks/sensingblocks.cpp
@@ -39,6 +39,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);
     engine->addCompileFunction(this, "sensing_timer", &compileTimer);
     engine->addCompileFunction(this, "sensing_resettimer", &compileResetTimer);
     engine->addCompileFunction(this, "sensing_of", &compileOf);
@@ -166,6 +167,11 @@ void SensingBlocks::compileLoudness(Compiler *compiler)
     compiler->addFunctionCall(&loudness);
 }
 
+void SensingBlocks::compileLoud(Compiler *compiler)
+{
+    compiler->addFunctionCall(&loud);
+}
+
 void SensingBlocks::compileTimer(Compiler *compiler)
 {
     compiler->addFunctionCall(&timer);
@@ -479,6 +485,16 @@ unsigned int SensingBlocks::loudness(VirtualMachine *vm)
     return 0;
 }
 
+unsigned int SensingBlocks::loud(VirtualMachine *vm)
+{
+    if (!audioInput)
+        audioInput = AudioInput::instance().get();
+
+    auto audioLoudness = audioInput->audioLoudness();
+    vm->addReturnValue(audioLoudness->getLoudness() > 10.0);
+    return 0;
+}
+
 unsigned int SensingBlocks::distanceTo(VirtualMachine *vm)
 {
     Sprite *sprite = dynamic_cast<Sprite *>(vm->target());
diff --git a/src/blocks/sensingblocks.h b/src/blocks/sensingblocks.h
index 62bdc6c1..c0cba564 100644
--- a/src/blocks/sensingblocks.h
+++ b/src/blocks/sensingblocks.h
@@ -68,6 +68,7 @@ class SensingBlocks : public IBlockSection
         static void compileMouseY(Compiler *compiler);
         static void compileSetDragMode(Compiler *compiler);
         static void compileLoudness(Compiler *compiler);
+        static void compileLoud(Compiler *compiler);
         static void compileTimer(Compiler *compiler);
         static void compileResetTimer(Compiler *compiler);
         static void compileOf(Compiler *compiler);
@@ -91,6 +92,7 @@ class SensingBlocks : public IBlockSection
         static unsigned int setNotDraggableMode(VirtualMachine *vm);
 
         static unsigned int loudness(VirtualMachine *vm);
+        static unsigned int loud(VirtualMachine *vm);
 
         static unsigned int distanceTo(VirtualMachine *vm);
         static unsigned int distanceToByIndex(VirtualMachine *vm);
diff --git a/test/blocks/sensing_blocks_test.cpp b/test/blocks/sensing_blocks_test.cpp
index e9797eb6..c94efc07 100644
--- a/test/blocks/sensing_blocks_test.cpp
+++ b/test/blocks/sensing_blocks_test.cpp
@@ -132,6 +132,7 @@ TEST_F(SensingBlocksTest, RegisterBlocks)
     EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "sensing_mousey", &SensingBlocks::compileMouseY));
     EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "sensing_setdragmode", &SensingBlocks::compileSetDragMode));
     EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "sensing_loudness", &SensingBlocks::compileLoudness));
+    EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "sensing_loud", &SensingBlocks::compileLoud));
     EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "sensing_timer", &SensingBlocks::compileTimer));
     EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "sensing_resettimer", &SensingBlocks::compileResetTimer));
     EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "sensing_of", &SensingBlocks::compileOf));
@@ -804,6 +805,84 @@ TEST_F(SensingBlocksTest, LoudnessImpl)
     SensingBlocks::audioInput = nullptr;
 }
 
+TEST_F(SensingBlocksTest, Loud)
+{
+    Compiler compiler(&m_engineMock);
+
+    auto block = std::make_shared<Block>("a", "sensing_loud");
+
+    EXPECT_CALL(m_engineMock, functionIndex(&SensingBlocks::loud)).WillOnce(Return(0));
+    compiler.init();
+
+    compiler.setBlock(block);
+    SensingBlocks::compileLoud(&compiler);
+
+    compiler.end();
+
+    ASSERT_EQ(compiler.bytecode(), std::vector<unsigned int>({ vm::OP_START, vm::OP_EXEC, 0, vm::OP_HALT }));
+    ASSERT_TRUE(compiler.constValues().empty());
+}
+
+TEST_F(SensingBlocksTest, LoudImpl)
+{
+    static unsigned int bytecode[] = { vm::OP_START, vm::OP_EXEC, 0, vm::OP_HALT };
+    static BlockFunc functions[] = { &SensingBlocks::loud };
+
+    VirtualMachine vm(nullptr, &m_engineMock, nullptr);
+    vm.setFunctions(functions);
+
+    vm.setBytecode(bytecode);
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 1);
+
+    AudioInputMock audioInput;
+    auto audioLoudness = std::make_shared<AudioLoudnessMock>();
+    SensingBlocks::audioInput = &audioInput;
+
+    EXPECT_CALL(audioInput, audioLoudness()).WillOnce(Return(audioLoudness));
+    EXPECT_CALL(*audioLoudness, getLoudness()).WillOnce(Return(-1));
+    vm.reset();
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 1);
+    ASSERT_FALSE(vm.getInput(0, 1)->toBool());
+
+    EXPECT_CALL(audioInput, audioLoudness()).WillOnce(Return(audioLoudness));
+    EXPECT_CALL(*audioLoudness, getLoudness()).WillOnce(Return(62));
+    vm.reset();
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 1);
+    ASSERT_TRUE(vm.getInput(0, 1)->toBool());
+
+    EXPECT_CALL(audioInput, audioLoudness()).WillOnce(Return(audioLoudness));
+    EXPECT_CALL(*audioLoudness, getLoudness()).WillOnce(Return(9));
+    vm.reset();
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 1);
+    ASSERT_FALSE(vm.getInput(0, 1)->toBool());
+
+    EXPECT_CALL(audioInput, audioLoudness()).WillOnce(Return(audioLoudness));
+    EXPECT_CALL(*audioLoudness, getLoudness()).WillOnce(Return(10));
+    vm.reset();
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 1);
+    ASSERT_FALSE(vm.getInput(0, 1)->toBool());
+
+    EXPECT_CALL(audioInput, audioLoudness()).WillOnce(Return(audioLoudness));
+    EXPECT_CALL(*audioLoudness, getLoudness()).WillOnce(Return(11));
+    vm.reset();
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 1);
+    ASSERT_TRUE(vm.getInput(0, 1)->toBool());
+
+    SensingBlocks::audioInput = nullptr;
+}
+
 TEST_F(SensingBlocksTest, Timer)
 {
     Compiler compiler(&m_engineMock);

From a57075c6e8e379f4f385edc872dc20dbe1b97801 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Sat, 16 Mar 2024 15:22:02 +0100
Subject: [PATCH 26/77] Add hatPredicateCompileFunction property to Block

---
 include/scratchcpp/block.h          |  3 +++
 include/scratchcpp/global.h         |  7 +++++++
 src/scratch/block.cpp               | 19 +++++++++++++++++++
 src/scratch/block_p.h               |  1 +
 test/scratch_classes/block_test.cpp |  9 +++++++++
 5 files changed, 39 insertions(+)

diff --git a/include/scratchcpp/block.h b/include/scratchcpp/block.h
index 2b34b96e..02b7ddeb 100644
--- a/include/scratchcpp/block.h
+++ b/include/scratchcpp/block.h
@@ -70,6 +70,9 @@ class LIBSCRATCHCPP_EXPORT Block : public Entity
         BlockComp compileFunction() const;
         void setCompileFunction(BlockComp newCompileFunction);
 
+        BlockComp hatPredicateCompileFunction() const;
+        void setHatPredicateCompileFunction(HatPredicateCompileFunc newHatPredicateCompileFunction);
+
         bool mutationHasNext() const;
         void setMutationHasNext(bool newMutationHasNext);
 
diff --git a/include/scratchcpp/global.h b/include/scratchcpp/global.h
index 6bf43b4f..91500012 100644
--- a/include/scratchcpp/global.h
+++ b/include/scratchcpp/global.h
@@ -62,6 +62,13 @@ using MonitorNameFunc = const std::string &(*)(Block *);
  */
 using MonitorChangeFunc = void (*)(Block *, const Value &newValue);
 
+/*!
+ * \typedef HatPredicateCompileFunc
+ *
+ * HatPredicateCompileFunc is a function pointer for functions which are used to compile edge-activated hat predicates to bytecode.
+ */
+using HatPredicateCompileFunc = void (*)(Compiler *vm);
+
 } // namespace libscratchcpp
 
 #endif // LIBSCRATCHCPP_GLOBAL_H
diff --git a/src/scratch/block.cpp b/src/scratch/block.cpp
index aa1165cf..6ce51593 100644
--- a/src/scratch/block.cpp
+++ b/src/scratch/block.cpp
@@ -4,6 +4,7 @@
 #include <scratchcpp/input.h>
 #include <scratchcpp/field.h>
 #include <scratchcpp/comment.h>
+#include <iostream>
 
 #include "block_p.h"
 
@@ -41,6 +42,24 @@ void Block::setCompileFunction(BlockComp newCompileFunction)
     impl->compileFunction = newCompileFunction;
 }
 
+/*! Returns the edge-activated hat predicate compile function. \see <a href="blockSections.html">Block sections</a> */
+BlockComp Block::hatPredicateCompileFunction() const
+{
+    return impl->hatPredicateCompileFunction;
+}
+
+/*! Sets the edge-activated hat predicate compile function. \see <a href="blockSections.html">Block sections</a> */
+void Block::setHatPredicateCompileFunction(HatPredicateCompileFunc newHatPredicateCompileFunction)
+{
+    if (newHatPredicateCompileFunction && !topLevel()) {
+        std::cerr << "error: predicate compile functions can be only used on top-level blocks" << std::endl;
+        assert(false);
+        return;
+    }
+
+    impl->hatPredicateCompileFunction = newHatPredicateCompileFunction;
+}
+
 /*! Returns true if the block can have a block following it. */
 bool Block::mutationHasNext() const
 {
diff --git a/src/scratch/block_p.h b/src/scratch/block_p.h
index 15615bc6..215d3bbd 100644
--- a/src/scratch/block_p.h
+++ b/src/scratch/block_p.h
@@ -23,6 +23,7 @@ struct BlockPrivate
 
         std::string opcode;
         BlockComp compileFunction = nullptr;
+        HatPredicateCompileFunc hatPredicateCompileFunction = nullptr;
         std::shared_ptr<Block> next = nullptr;
         std::string nextId;
         std::shared_ptr<Block> parent = nullptr;
diff --git a/test/scratch_classes/block_test.cpp b/test/scratch_classes/block_test.cpp
index e762b77f..905f5c64 100644
--- a/test/scratch_classes/block_test.cpp
+++ b/test/scratch_classes/block_test.cpp
@@ -247,6 +247,15 @@ TEST_F(BlockTest, CompileFunction)
     ASSERT_EQ(block.compileFunction(), &compileTest);
 }
 
+TEST_F(BlockTest, HatPredicateCompileFunction)
+{
+    Block block("", "");
+    ASSERT_EQ(block.hatPredicateCompileFunction(), nullptr);
+
+    block.setHatPredicateCompileFunction(&compileTest);
+    ASSERT_EQ(block.hatPredicateCompileFunction(), &compileTest);
+}
+
 TEST_F(BlockTest, Compile)
 {
     Block block("", "");

From c25b80f0a95310edd03f3875542349dab179dfe3 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Sat, 16 Mar 2024 15:40:32 +0100
Subject: [PATCH 27/77] Add edge-activated hat support to Compiler

---
 include/scratchcpp/compiler.h   |  1 +
 src/engine/compiler.cpp         | 26 ++++++++++++++++++++++++++
 src/engine/compiler_p.h         |  1 +
 test/compiler/compiler_test.cpp | 21 +++++++++++++++++++++
 4 files changed, 49 insertions(+)

diff --git a/include/scratchcpp/compiler.h b/include/scratchcpp/compiler.h
index f96cf2af..a8b852bb 100644
--- a/include/scratchcpp/compiler.h
+++ b/include/scratchcpp/compiler.h
@@ -42,6 +42,7 @@ class LIBSCRATCHCPP_EXPORT Compiler
         void end();
 
         const std::vector<unsigned int> &bytecode() const;
+        const std::vector<unsigned int> &hatPredicateBytecode() const;
 
         IEngine *engine() const;
         Target *target() const;
diff --git a/src/engine/compiler.cpp b/src/engine/compiler.cpp
index d36c0074..dae779d2 100644
--- a/src/engine/compiler.cpp
+++ b/src/engine/compiler.cpp
@@ -41,6 +41,26 @@ void Compiler::compile(std::shared_ptr<Block> topLevelBlock)
     init();
 
     impl->block = topLevelBlock;
+
+    // If this is a top-level block, it might be an edge-activated hat block with a predicate compile function
+    if (impl->block->topLevel()) {
+        HatPredicateCompileFunc f = impl->block->hatPredicateCompileFunction();
+
+        if (f) {
+            f(this);
+
+            // Workaround for register leak warning spam: pause the script after getting the reported value
+            addFunctionCall([](VirtualMachine *vm) -> unsigned int {
+                vm->stop(false, false, false);
+                return 0;
+            });
+
+            end(); // finished with the predicate
+            impl->hatPredicateBytecode = impl->bytecode;
+            init(); // now start the real compilation
+        }
+    }
+
     while (impl->block) {
         size_t substacks = impl->substackTree.size();
 
@@ -80,6 +100,12 @@ const std::vector<unsigned int> &Compiler::bytecode() const
     return impl->bytecode;
 }
 
+/*! Returns the generated hat predicate bytecode (if this is an edge-activated hat). */
+const std::vector<unsigned int> &Compiler::hatPredicateBytecode() const
+{
+    return impl->hatPredicateBytecode;
+}
+
 /*! Returns the Engine. */
 IEngine *Compiler::engine() const
 {
diff --git a/src/engine/compiler_p.h b/src/engine/compiler_p.h
index 429db205..a310eb47 100644
--- a/src/engine/compiler_p.h
+++ b/src/engine/compiler_p.h
@@ -28,6 +28,7 @@ struct CompilerPrivate
         bool initialized = false;
 
         std::vector<unsigned int> bytecode;
+        std::vector<unsigned int> hatPredicateBytecode;
         std::vector<InputValue *> constValues;
         std::vector<std::unique_ptr<InputValue>> customConstValues;
         std::unordered_map<InputValue *, std::pair<bool, std::string>> constValueMenuInfo; // input value, <whether the input points to a dropdown menu, selected menu item>
diff --git a/test/compiler/compiler_test.cpp b/test/compiler/compiler_test.cpp
index 0d517d04..dde1626a 100644
--- a/test/compiler/compiler_test.cpp
+++ b/test/compiler/compiler_test.cpp
@@ -816,3 +816,24 @@ TEST_F(CompilerTest, MultipleTargets)
     ASSERT_EQ(vm->engine(), &engine);
     ASSERT_EQ(vm->script(), script.get());
 }
+
+TEST_F(CompilerTest, EdgeActivatedHatPredicate)
+{
+    auto hat = std::make_shared<Block>("", "");
+    hat->setCompileFunction([](Compiler *) {});
+    hat->setHatPredicateCompileFunction([](Compiler *compiler) { compiler->addConstValue(true); });
+
+    auto block = std::make_shared<Block>("", "");
+    block->setCompileFunction([](Compiler *compiler) {
+        compiler->addConstValue("test");
+        compiler->addInstruction(vm::OP_PRINT);
+    });
+    block->setParent(hat);
+    hat->setNext(block);
+
+    INIT_COMPILER(engine, compiler);
+    compiler.compile(hat);
+    ASSERT_EQ(compiler.hatPredicateBytecode(), std::vector<unsigned int>({ vm::OP_START, vm::OP_CONST, 0, vm::OP_EXEC, 0, vm::OP_HALT }));
+    ASSERT_EQ(compiler.bytecode(), std::vector<unsigned int>({ vm::OP_START, vm::OP_CONST, 1, vm::OP_PRINT, vm::OP_HALT }));
+    ASSERT_EQ(compiler.constValues(), std::vector<Value>({ true, "test" }));
+}

From d1a45d1526e01ad8368690ec4868b83fbf1e890e Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Sat, 16 Mar 2024 16:32:21 +0100
Subject: [PATCH 28/77] Add hat predicate support to Script

---
 include/scratchcpp/script.h |  3 ++
 src/engine/script.cpp       | 38 ++++++++++++++++++++
 src/engine/script_p.h       |  3 ++
 test/script/script_test.cpp | 70 ++++++++++++++++++++++++++++++++++++-
 4 files changed, 113 insertions(+), 1 deletion(-)

diff --git a/include/scratchcpp/script.h b/include/scratchcpp/script.h
index bd80c86e..010906f5 100644
--- a/include/scratchcpp/script.h
+++ b/include/scratchcpp/script.h
@@ -34,6 +34,9 @@ class LIBSCRATCHCPP_EXPORT Script
         const std::vector<unsigned int> &bytecodeVector() const;
         void setBytecode(const std::vector<unsigned int> &code);
 
+        void setHatPredicateBytecode(const std::vector<unsigned int> &code);
+        bool runHatPredicate();
+
         void setProcedures(const std::vector<unsigned int *> &procedures);
         void setFunctions(const std::vector<BlockFunc> &functions);
         void setConstValues(const std::vector<Value> &values);
diff --git a/src/engine/script.cpp b/src/engine/script.cpp
index 52d4df6b..c770a23a 100644
--- a/src/engine/script.cpp
+++ b/src/engine/script.cpp
@@ -6,6 +6,7 @@
 #include <scratchcpp/list.h>
 #include <scratchcpp/sprite.h>
 #include <scratchcpp/iengine.h>
+#include <scratchcpp/stage.h>
 #include <iostream>
 
 #include "script_p.h"
@@ -48,6 +49,37 @@ void Script::setBytecode(const std::vector<unsigned int> &code)
     impl->bytecode = impl->bytecodeVector.data();
 }
 
+/*! Sets the edge-activated hat predicate bytecode. */
+void Script::setHatPredicateBytecode(const std::vector<unsigned int> &code)
+{
+    impl->hatPredicateBytecodeVector = code;
+
+    if (impl->engine && !code.empty()) {
+        impl->hatPredicateVm = std::make_shared<VirtualMachine>(impl->engine->stage(), impl->engine, this);
+        impl->hatPredicateVm->setBytecode(impl->hatPredicateBytecodeVector.data());
+        impl->hatPredicateVm->setFunctions(impl->functions);
+        impl->hatPredicateVm->setConstValues(impl->constValues);
+    }
+}
+
+/*!
+ * Runs the edge-activated hat predicate and returns the reported value.
+ * \note If there isn't any predicate, nothing will happen and the returned value will be false.
+ */
+bool Script::runHatPredicate()
+{
+    if (impl->hatPredicateVm && impl->hatPredicateVm->bytecode()) {
+        impl->hatPredicateVm->reset();
+        impl->hatPredicateVm->run();
+        assert(impl->hatPredicateVm->registerCount() == 1);
+
+        if (impl->hatPredicateVm->registerCount() == 1)
+            return impl->hatPredicateVm->getInput(0, 1)->toBool();
+    }
+
+    return false;
+}
+
 /*! Starts the script (creates a virtual machine). */
 std::shared_ptr<VirtualMachine> Script::start()
 {
@@ -129,6 +161,9 @@ void Script::setFunctions(const std::vector<BlockFunc> &functions)
 {
     impl->functionsVector = functions;
     impl->functions = impl->functionsVector.data();
+
+    if (impl->hatPredicateVm)
+        impl->hatPredicateVm->setFunctions(impl->functions);
 }
 
 /*! Sets the list of constant values. */
@@ -136,6 +171,9 @@ void Script::setConstValues(const std::vector<Value> &values)
 {
     impl->constValuesVector = values;
     impl->constValues = impl->constValuesVector.data();
+
+    if (impl->hatPredicateVm)
+        impl->hatPredicateVm->setConstValues(impl->constValues);
 }
 
 /*! Sets the list of variables. */
diff --git a/src/engine/script_p.h b/src/engine/script_p.h
index b21d6517..3d7ca6a1 100644
--- a/src/engine/script_p.h
+++ b/src/engine/script_p.h
@@ -23,6 +23,9 @@ struct ScriptPrivate
         unsigned int *bytecode = nullptr;
         std::vector<unsigned int> bytecodeVector;
 
+        std::vector<unsigned int> hatPredicateBytecodeVector;
+        std::shared_ptr<VirtualMachine> hatPredicateVm;
+
         Target *target = nullptr;
         std::shared_ptr<Block> topBlock;
         IEngine *engine = nullptr;
diff --git a/test/script/script_test.cpp b/test/script/script_test.cpp
index 760fa6f6..4fb5bbae 100644
--- a/test/script/script_test.cpp
+++ b/test/script/script_test.cpp
@@ -1,8 +1,8 @@
 #include <scratchcpp/script.h>
 #include <scratchcpp/block.h>
 #include <scratchcpp/virtualmachine.h>
-#include <scratchcpp/target.h>
 #include <scratchcpp/sprite.h>
+#include <scratchcpp/stage.h>
 #include <scratchcpp/variable.h>
 #include <scratchcpp/list.h>
 #include <enginemock.h>
@@ -41,6 +41,74 @@ TEST_F(ScriptTest, Bytecode)
     ASSERT_EQ(script.bytecodeVector(), std::vector<unsigned int>({ vm::OP_START, vm::OP_HALT }));
 }
 
+static Target *stageTest = nullptr;
+static IEngine *engineTest = nullptr;
+static Script *scriptTest = nullptr;
+
+TEST_F(ScriptTest, HatPredicate)
+{
+    Script script(&m_target, nullptr, &m_engine);
+    ASSERT_FALSE(script.runHatPredicate());
+
+    Stage stage;
+    EXPECT_CALL(m_engine, stage()).Times(3).WillRepeatedly(Return(&stage));
+
+    BlockFunc f1 = [](VirtualMachine *vm) -> unsigned int {
+        vm->addReturnValue(true);
+        vm->stop(false, false, false);
+        stageTest = vm->target();
+        engineTest = vm->engine();
+        scriptTest = vm->script();
+        return 0;
+    };
+
+    BlockFunc f2 = f1;
+
+    BlockFunc f3 = [](VirtualMachine *vm) -> unsigned int {
+        vm->addReturnValue(false);
+        vm->stop(false, false, false);
+        stageTest = vm->target();
+        engineTest = vm->engine();
+        scriptTest = vm->script();
+        return 0;
+    };
+
+    stageTest = nullptr;
+    engineTest = nullptr;
+    scriptTest = nullptr;
+    script.setFunctions({ f1 });
+    script.setConstValues({ "test" });
+    script.setHatPredicateBytecode({ vm::OP_START, vm::OP_CONST, 0, vm::OP_PRINT, vm::OP_EXEC, 0, vm::OP_HALT });
+    testing::internal::CaptureStdout();
+    ASSERT_TRUE(script.runHatPredicate());
+    ASSERT_EQ(testing::internal::GetCapturedStdout(), "test\n");
+    ASSERT_EQ(stageTest, &stage);
+    ASSERT_EQ(engineTest, &m_engine);
+    ASSERT_EQ(scriptTest, &script);
+
+    stageTest = nullptr;
+    engineTest = nullptr;
+    scriptTest = nullptr;
+    script.setHatPredicateBytecode({ vm::OP_START, vm::OP_CONST, 0, vm::OP_PRINT, vm::OP_EXEC, 1, vm::OP_HALT });
+    script.setFunctions({ f1, f2, f3 });
+    script.setConstValues({ 5 });
+    testing::internal::CaptureStdout();
+    ASSERT_TRUE(script.runHatPredicate());
+    ASSERT_EQ(testing::internal::GetCapturedStdout(), "5\n");
+    ASSERT_EQ(stageTest, &stage);
+    ASSERT_EQ(engineTest, &m_engine);
+    ASSERT_EQ(scriptTest, &script);
+
+    stageTest = nullptr;
+    engineTest = nullptr;
+    scriptTest = nullptr;
+    script.setHatPredicateBytecode({ vm::OP_START, vm::OP_EXEC, 2, vm::OP_HALT });
+    ASSERT_FALSE(script.runHatPredicate());
+    ASSERT_EQ(stageTest, &stage);
+    ASSERT_EQ(engineTest, &m_engine);
+    ASSERT_EQ(scriptTest, &script);
+}
+
 unsigned int testFunction(VirtualMachine *)
 {
     return 0;

From 7a10ac0e103a4436ae2516c713bc480266f9d6d7 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Sat, 16 Mar 2024 16:37:19 +0100
Subject: [PATCH 29/77] Add edge-activated hat API to IEngine

---
 include/scratchcpp/iengine.h                  |  7 +++
 src/engine/internal/blocksectioncontainer.cpp | 12 +++++
 src/engine/internal/blocksectioncontainer.h   |  3 ++
 src/engine/internal/engine.cpp                | 52 ++++++++++++++++---
 src/engine/internal/engine.h                  |  4 ++
 test/mocks/enginemock.h                       |  1 +
 6 files changed, 72 insertions(+), 7 deletions(-)

diff --git a/include/scratchcpp/iengine.h b/include/scratchcpp/iengine.h
index 7361ee40..43830914 100644
--- a/include/scratchcpp/iengine.h
+++ b/include/scratchcpp/iengine.h
@@ -223,6 +223,13 @@ class LIBSCRATCHCPP_EXPORT IEngine
          */
         virtual void addCompileFunction(IBlockSection *section, const std::string &opcode, BlockComp f) = 0;
 
+        /*!
+         * Call this from IBlockSection#registerBlocks() to add a hat block predicate compile function to a block section.
+         * \note This only works with edge-activated hats.
+         * \see <a href="blockSections.html">Block sections</a>
+         */
+        virtual void addHatPredicateCompileFunction(IBlockSection *section, const std::string &opcode, HatPredicateCompileFunc f) = 0;
+
         /*!
          * Call this from IBlockSection#registerBlocks() to add a monitor name function to a block section.
          * \see <a href="blockSections.html">Block sections</a>
diff --git a/src/engine/internal/blocksectioncontainer.cpp b/src/engine/internal/blocksectioncontainer.cpp
index a258d82a..aef43d5b 100644
--- a/src/engine/internal/blocksectioncontainer.cpp
+++ b/src/engine/internal/blocksectioncontainer.cpp
@@ -11,6 +11,11 @@ void BlockSectionContainer::addCompileFunction(const std::string &opcode, BlockC
     m_compileFunctions[opcode] = f;
 }
 
+void BlockSectionContainer::addHatPredicateCompileFunction(const std::string &opcode, HatPredicateCompileFunc f)
+{
+    m_hatPredicateCompileFunctions[opcode] = f;
+}
+
 void BlockSectionContainer::addMonitorNameFunction(const std::string &opcode, MonitorNameFunc f)
 {
     m_monitorNameFunctions[opcode] = f;
@@ -48,6 +53,13 @@ BlockComp BlockSectionContainer::resolveBlockCompileFunc(const std::string &opco
     return nullptr;
 }
 
+HatPredicateCompileFunc BlockSectionContainer::resolveHatPredicateCompileFunc(const std::string &opcode) const
+{
+    if (m_hatPredicateCompileFunctions.count(opcode) == 1)
+        return m_hatPredicateCompileFunctions.at(opcode);
+    return nullptr;
+}
+
 MonitorNameFunc BlockSectionContainer::resolveMonitorNameFunc(const std::string &opcode) const
 {
     if (m_monitorNameFunctions.count(opcode) == 1)
diff --git a/src/engine/internal/blocksectioncontainer.h b/src/engine/internal/blocksectioncontainer.h
index eca60fc4..37026f3f 100644
--- a/src/engine/internal/blocksectioncontainer.h
+++ b/src/engine/internal/blocksectioncontainer.h
@@ -16,6 +16,7 @@ class BlockSectionContainer
         BlockSectionContainer(const BlockSectionContainer &) = delete;
 
         void addCompileFunction(const std::string &opcode, BlockComp f);
+        void addHatPredicateCompileFunction(const std::string &opcode, HatPredicateCompileFunc f);
         void addMonitorNameFunction(const std::string &opcode, MonitorNameFunc f);
         void addMonitorChangeFunction(const std::string &opcode, MonitorChangeFunc f);
         void addHatBlock(const std::string &opcode);
@@ -24,6 +25,7 @@ class BlockSectionContainer
         void addFieldValue(const std::string &value, int id);
 
         BlockComp resolveBlockCompileFunc(const std::string &opcode) const;
+        HatPredicateCompileFunc resolveHatPredicateCompileFunc(const std::string &opcode) const;
         MonitorNameFunc resolveMonitorNameFunc(const std::string &opcode) const;
         MonitorChangeFunc resolveMonitorChangeFunc(const std::string &opcode) const;
         int resolveInput(const std::string &name) const;
@@ -32,6 +34,7 @@ class BlockSectionContainer
 
     private:
         std::unordered_map<std::string, BlockComp> m_compileFunctions;
+        std::unordered_map<std::string, HatPredicateCompileFunc> m_hatPredicateCompileFunctions;
         std::unordered_map<std::string, MonitorNameFunc> m_monitorNameFunctions;
         std::unordered_map<std::string, MonitorChangeFunc> m_monitorChangeFunctions;
         std::unordered_map<std::string, int> m_inputs;
diff --git a/src/engine/internal/engine.cpp b/src/engine/internal/engine.cpp
index 6ab9c3d1..b656ffdb 100644
--- a/src/engine/internal/engine.cpp
+++ b/src/engine/internal/engine.cpp
@@ -38,6 +38,11 @@ const std::unordered_map<Engine::HatType, bool> Engine::m_hatRestartExistingThre
     { HatType::CloneInit, false }, { HatType::KeyPressed, false },       { HatType::TargetClicked, true }
 };
 
+const std::unordered_map<Engine::HatType, bool> Engine::m_hatEdgeActivated = {
+    { HatType::GreenFlag, false }, { HatType::BroadcastReceived, false }, { HatType::BackdropChanged, false },
+    { HatType::CloneInit, false }, { HatType::KeyPressed, false },        { HatType::TargetClicked, false }
+};
+
 Engine::Engine() :
     m_defaultTimer(std::make_unique<Timer>()),
     m_timer(m_defaultTimer.get()),
@@ -79,6 +84,7 @@ void Engine::clear()
     m_whenKeyPressedHats.clear();
 
     m_scriptHatFields.clear();
+    m_edgeActivatedHatValues.clear();
 
     m_running = false;
 }
@@ -93,8 +99,11 @@ void Engine::resolveIds()
             auto container = blockSectionContainer(block->opcode());
             block->setNext(getBlock(block->nextId()));
             block->setParent(getBlock(block->parentId()));
-            if (container)
+
+            if (container) {
                 block->setCompileFunction(container->resolveBlockCompileFunc(block->opcode()));
+                block->setHatPredicateCompileFunction(container->resolveHatPredicateCompileFunc(block->opcode()));
+            }
 
             const auto &inputs = block->inputs();
             for (const auto &input : inputs) {
@@ -215,6 +224,8 @@ void Engine::compile()
                     compiler.compile(block);
 
                     script->setBytecode(compiler.bytecode());
+                    script->setHatPredicateBytecode(compiler.hatPredicateBytecode());
+
                     if (block->opcode() == "procedures_definition") {
                         auto b = block->inputAt(block->findInput("custom_block"))->valueBlock();
                         procedureBytecodeMap[b->mutationPrototype()->procCode()] = script->bytecode();
@@ -295,6 +306,7 @@ void Engine::start()
 
     m_eventLoopMutex.lock();
     m_timer->reset();
+    m_edgeActivatedHatValues.clear();
     m_running = true;
 
     // Start "when green flag clicked" scripts
@@ -432,6 +444,30 @@ void Engine::step()
     // Clean up threads that were told to stop during or since the last step
     m_threads.erase(std::remove_if(m_threads.begin(), m_threads.end(), [](std::shared_ptr<VirtualMachine> thread) { return thread->atEnd(); }), m_threads.end());
 
+    // Find all edge-activated hats, and add them to threads to be evaluated
+    for (auto const &[hatType, edgeActivated] : m_hatEdgeActivated) {
+        if (edgeActivated) {
+            auto newThreads = startHats(hatType, {}, nullptr);
+
+            // Process edge-triggered hats (must happen here because of Scratch 2 compatibility)
+            // Processing the hats means running their predicates (if they didn't change their return value from false to true, remove the threads)
+            for (auto thread : newThreads) {
+                bool oldValue = false;
+                auto it = m_edgeActivatedHatValues.find(hatType);
+
+                if (it != m_edgeActivatedHatValues.cend())
+                    oldValue = it->second;
+
+                bool newValue = thread->script()->runHatPredicate();
+                bool edgeWasActivated = !oldValue && newValue; // changed from false true
+                m_edgeActivatedHatValues[hatType] = newValue;
+
+                if (!edgeWasActivated)
+                    stopThread(thread.get());
+            }
+        }
+    }
+
     m_redrawRequested = false;
 
     // Step threads
@@ -856,6 +892,14 @@ void Engine::addCompileFunction(IBlockSection *section, const std::string &opcod
         container->addCompileFunction(opcode, f);
 }
 
+void Engine::addHatPredicateCompileFunction(IBlockSection *section, const std::string &opcode, HatPredicateCompileFunc f)
+{
+    auto container = blockSectionContainer(section);
+
+    if (container)
+        container->addHatPredicateCompileFunction(opcode, f);
+}
+
 void Engine::addMonitorNameFunction(IBlockSection *section, const std::string &opcode, MonitorNameFunc f)
 {
     auto container = blockSectionContainer(section);
@@ -1718,11 +1762,5 @@ Engine::startHats(HatType hatType, const std::unordered_map<HatField, std::varia
         },
         optTarget);
 
-    // Run edge-triggered hats (for compatibility with Scratch 2)
-    // TODO: Find out what "edge-triggered" hats are and execute them
-    // Uncommenting this would cause infinite recursion in some cases, so let's keep it commented
-    /*for (auto thread : newThreads)
-        thread->run();*/
-
     return newThreads;
 }
diff --git a/src/engine/internal/engine.h b/src/engine/internal/engine.h
index 26800535..84ef9508 100644
--- a/src/engine/internal/engine.h
+++ b/src/engine/internal/engine.h
@@ -105,6 +105,7 @@ class Engine : public IEngine
         unsigned int functionIndex(BlockFunc f) override;
 
         void addCompileFunction(IBlockSection *section, const std::string &opcode, BlockComp f) override;
+        void addHatPredicateCompileFunction(IBlockSection *section, const std::string &opcode, HatPredicateCompileFunc f) override;
         void addMonitorNameFunction(IBlockSection *section, const std::string &opcode, MonitorNameFunc f) override;
         void addMonitorChangeFunction(IBlockSection *section, const std::string &opcode, MonitorChangeFunc f) override;
         void addHatBlock(IBlockSection *section, const std::string &opcode) override;
@@ -216,6 +217,7 @@ class Engine : public IEngine
         startHats(HatType hatType, const std::unordered_map<HatField, std::variant<std::string, const char *, Entity *>> &optMatchFields, Target *optTarget);
 
         static const std::unordered_map<HatType, bool> m_hatRestartExistingThreads; // used to check whether a hat should restart existing threads
+        static const std::unordered_map<HatType, bool> m_hatEdgeActivated;          // used to check whether a hat is edge-activated (runs when a predicate becomes true)
 
         std::unordered_map<std::shared_ptr<IBlockSection>, std::unique_ptr<BlockSectionContainer>> m_sections;
         std::vector<std::shared_ptr<Target>> m_targets;
@@ -241,6 +243,8 @@ class Engine : public IEngine
 
         std::unordered_map<Script *, std::unordered_map<HatField, int>> m_scriptHatFields; // HatField, field ID from the block implementation
 
+        std::unordered_map<HatType, bool> m_edgeActivatedHatValues; // edge-activated hats only run after the value changes from false to true
+
         std::unique_ptr<ITimer> m_defaultTimer;
         ITimer *m_timer = nullptr;
         double m_fps = 30;                         // default FPS
diff --git a/test/mocks/enginemock.h b/test/mocks/enginemock.h
index b7a89755..d744c497 100644
--- a/test/mocks/enginemock.h
+++ b/test/mocks/enginemock.h
@@ -86,6 +86,7 @@ class EngineMock : public IEngine
         MOCK_METHOD(unsigned int, functionIndex, (BlockFunc), (override));
 
         MOCK_METHOD(void, addCompileFunction, (IBlockSection *, const std::string &, BlockComp), (override));
+        MOCK_METHOD(void, addHatPredicateCompileFunction, (IBlockSection *, const std::string &, HatPredicateCompileFunc), (override));
         MOCK_METHOD(void, addMonitorNameFunction, (IBlockSection *, const std::string &, MonitorNameFunc), (override));
         MOCK_METHOD(void, addMonitorChangeFunction, (IBlockSection *, const std::string &, MonitorChangeFunc), (override));
         MOCK_METHOD(void, addHatBlock, (IBlockSection *, const std::string &), (override));

From 096e5d8e086381f6c2719983f20e10793c9d9b00 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Sat, 16 Mar 2024 17:44:10 +0100
Subject: [PATCH 30/77] Implement event_whengreaterthan block

---
 include/scratchcpp/iengine.h      |   3 +
 src/blocks/eventblocks.cpp        |  66 +++++++++++++
 src/blocks/eventblocks.h          |  18 +++-
 src/engine/internal/engine.cpp    |  19 +++-
 src/engine/internal/engine.h      |   8 +-
 test/blocks/event_blocks_test.cpp | 154 +++++++++++++++++++++++++++++-
 test/engine/engine_test.cpp       |  31 ++++++
 test/mocks/enginemock.h           |   1 +
 test/when_greater_than.sb3        | Bin 0 -> 943 bytes
 9 files changed, 291 insertions(+), 9 deletions(-)
 create mode 100644 test/when_greater_than.sb3

diff --git a/include/scratchcpp/iengine.h b/include/scratchcpp/iengine.h
index 43830914..9e559420 100644
--- a/include/scratchcpp/iengine.h
+++ b/include/scratchcpp/iengine.h
@@ -299,6 +299,9 @@ class LIBSCRATCHCPP_EXPORT IEngine
         /* Registers the given "when this sprite/stage clicked" script. */
         virtual void addTargetClickScript(std::shared_ptr<Block> hatBlock) = 0;
 
+        /* Registers the given "when greater than" script. */
+        virtual void addWhenGreaterThanScript(std::shared_ptr<Block> hatBlock) = 0;
+
         /*! Returns the list of targets. */
         virtual const std::vector<std::shared_ptr<Target>> &targets() const = 0;
 
diff --git a/src/blocks/eventblocks.cpp b/src/blocks/eventblocks.cpp
index 81f925ef..282437c6 100644
--- a/src/blocks/eventblocks.cpp
+++ b/src/blocks/eventblocks.cpp
@@ -8,11 +8,17 @@
 #include <scratchcpp/field.h>
 #include <scratchcpp/stage.h>
 #include <scratchcpp/costume.h>
+#include <scratchcpp/block.h>
+#include <scratchcpp/itimer.h>
 
 #include "eventblocks.h"
+#include "audio/audioinput.h"
+#include "audio/iaudioloudness.h"
 
 using namespace libscratchcpp;
 
+IAudioInput *EventBlocks::audioInput = nullptr;
+
 std::string EventBlocks::name() const
 {
     return "Events";
@@ -28,15 +34,25 @@ void EventBlocks::registerBlocks(IEngine *engine)
     engine->addCompileFunction(this, "event_broadcastandwait", &compileBroadcastAndWait);
     engine->addCompileFunction(this, "event_whenbroadcastreceived", &compileWhenBroadcastReceived);
     engine->addCompileFunction(this, "event_whenbackdropswitchesto", &compileWhenBackdropSwitchesTo);
+    engine->addCompileFunction(this, "event_whengreaterthan", &compileWhenGreaterThan);
     engine->addCompileFunction(this, "event_whenkeypressed", &compileWhenKeyPressed);
 
+    // Hat predicates
+    engine->addHatPredicateCompileFunction(this, "event_whengreaterthan", &compileWhenGreaterThanPredicate);
+
     // Inputs
     engine->addInput(this, "BROADCAST_INPUT", BROADCAST_INPUT);
+    engine->addInput(this, "VALUE", VALUE);
 
     // Fields
     engine->addField(this, "BROADCAST_OPTION", BROADCAST_OPTION);
     engine->addField(this, "BACKDROP", BACKDROP);
+    engine->addField(this, "WHENGREATERTHANMENU", WHENGREATERTHANMENU);
     engine->addField(this, "KEY_OPTION", KEY_OPTION);
+
+    // Fields values
+    engine->addFieldValue(this, "LOUDNESS", Loudness);
+    engine->addFieldValue(this, "TIMER", Timer);
 }
 
 void EventBlocks::compileWhenFlagClicked(Compiler *compiler)
@@ -97,6 +113,38 @@ void EventBlocks::compileWhenBackdropSwitchesTo(Compiler *compiler)
     compiler->engine()->addBackdropChangeScript(compiler->block(), BACKDROP);
 }
 
+void EventBlocks::compileWhenGreaterThanPredicate(Compiler *compiler)
+{
+    Field *field = compiler->field(WHENGREATERTHANMENU);
+    BlockFunc predicate = nullptr;
+
+    if (field) {
+        switch (field->specialValueId()) {
+            case Loudness:
+                predicate = &whenLoudnessGreaterThanPredicate;
+                break;
+
+            case Timer:
+                predicate = &whenTimerGreaterThanPredicate;
+                break;
+
+            default:
+                compiler->addInstruction(vm::OP_NULL);
+                return;
+        }
+    }
+
+    if (predicate) {
+        compiler->addInput(VALUE);
+        compiler->addFunctionCall(predicate);
+    }
+}
+
+void EventBlocks::compileWhenGreaterThan(Compiler *compiler)
+{
+    compiler->engine()->addWhenGreaterThanScript(compiler->block());
+}
+
 void EventBlocks::compileWhenKeyPressed(Compiler *compiler)
 {
     // NOTE: Field values don't have to be registered because keys are referenced by their names
@@ -140,3 +188,21 @@ unsigned int EventBlocks::checkBroadcastByIndex(VirtualMachine *vm)
         vm->stop(true, true, true);
     return 1;
 }
+
+unsigned int EventBlocks::whenLoudnessGreaterThanPredicate(VirtualMachine *vm)
+{
+    if (!audioInput)
+        audioInput = AudioInput::instance().get();
+
+    auto audioLoudness = audioInput->audioLoudness();
+    const Value &operand = *vm->getInput(0, 1);
+    vm->replaceReturnValue(Value(audioLoudness->getLoudness()) > operand, 1);
+    return 0;
+}
+
+unsigned int EventBlocks::whenTimerGreaterThanPredicate(VirtualMachine *vm)
+{
+    const Value &operand = *vm->getInput(0, 1);
+    vm->replaceReturnValue(Value(vm->engine()->timer()->value()) > operand, 1);
+    return 0;
+}
diff --git a/src/blocks/eventblocks.h b/src/blocks/eventblocks.h
index ec63bdf8..49479da7 100644
--- a/src/blocks/eventblocks.h
+++ b/src/blocks/eventblocks.h
@@ -9,6 +9,7 @@ namespace libscratchcpp
 
 class Compiler;
 class VirtualMachine;
+class IAudioInput;
 
 /*! \brief The EventBlocks class contains the implementation of event blocks. */
 class EventBlocks : public IBlockSection
@@ -16,16 +17,24 @@ class EventBlocks : public IBlockSection
     public:
         enum Inputs
         {
-            BROADCAST_INPUT
+            BROADCAST_INPUT,
+            VALUE
         };
 
         enum Fields
         {
             BROADCAST_OPTION,
             BACKDROP,
+            WHENGREATERTHANMENU,
             KEY_OPTION
         };
 
+        enum FieldValues
+        {
+            Loudness,
+            Timer
+        };
+
         std::string name() const override;
 
         void registerBlocks(IEngine *engine) override;
@@ -37,6 +46,8 @@ class EventBlocks : public IBlockSection
         static void compileBroadcastAndWait(Compiler *compiler);
         static void compileWhenBroadcastReceived(Compiler *compiler);
         static void compileWhenBackdropSwitchesTo(Compiler *compiler);
+        static void compileWhenGreaterThanPredicate(Compiler *compiler);
+        static void compileWhenGreaterThan(Compiler *compiler);
         static void compileWhenKeyPressed(Compiler *compiler);
 
         static unsigned int broadcast(VirtualMachine *vm);
@@ -45,6 +56,11 @@ class EventBlocks : public IBlockSection
         static unsigned int broadcastByIndexAndWait(VirtualMachine *vm);
         static unsigned int checkBroadcast(VirtualMachine *vm);
         static unsigned int checkBroadcastByIndex(VirtualMachine *vm);
+
+        static unsigned int whenLoudnessGreaterThanPredicate(VirtualMachine *vm);
+        static unsigned int whenTimerGreaterThanPredicate(VirtualMachine *vm);
+
+        static IAudioInput *audioInput;
 };
 
 } // namespace libscratchcpp
diff --git a/src/engine/internal/engine.cpp b/src/engine/internal/engine.cpp
index b656ffdb..a810a99d 100644
--- a/src/engine/internal/engine.cpp
+++ b/src/engine/internal/engine.cpp
@@ -34,13 +34,13 @@
 using namespace libscratchcpp;
 
 const std::unordered_map<Engine::HatType, bool> Engine::m_hatRestartExistingThreads = {
-    { HatType::GreenFlag, true },  { HatType::BroadcastReceived, true }, { HatType::BackdropChanged, true },
-    { HatType::CloneInit, false }, { HatType::KeyPressed, false },       { HatType::TargetClicked, true }
+    { HatType::GreenFlag, true },   { HatType::BroadcastReceived, true }, { HatType::BackdropChanged, true }, { HatType::CloneInit, false },
+    { HatType::KeyPressed, false }, { HatType::TargetClicked, true },     { HatType::WhenGreaterThan, false }
 };
 
 const std::unordered_map<Engine::HatType, bool> Engine::m_hatEdgeActivated = {
-    { HatType::GreenFlag, false }, { HatType::BroadcastReceived, false }, { HatType::BackdropChanged, false },
-    { HatType::CloneInit, false }, { HatType::KeyPressed, false },        { HatType::TargetClicked, false }
+    { HatType::GreenFlag, false },  { HatType::BroadcastReceived, false }, { HatType::BackdropChanged, false }, { HatType::CloneInit, false },
+    { HatType::KeyPressed, false }, { HatType::TargetClicked, false },     { HatType::WhenGreaterThan, true }
 };
 
 Engine::Engine() :
@@ -82,6 +82,8 @@ void Engine::clear()
     m_broadcastHats.clear();
     m_cloneInitHats.clear();
     m_whenKeyPressedHats.clear();
+    m_whenTargetClickedHats.clear();
+    m_whenGreaterThanHats.clear();
 
     m_scriptHatFields.clear();
     m_edgeActivatedHatValues.clear();
@@ -1034,6 +1036,12 @@ void Engine::addTargetClickScript(std::shared_ptr<Block> hatBlock)
     addHatToMap(m_whenTargetClickedHats, m_scripts[hatBlock].get());
 }
 
+void Engine::addWhenGreaterThanScript(std::shared_ptr<Block> hatBlock)
+{
+    Script *script = m_scripts[hatBlock].get();
+    addHatToMap(m_whenGreaterThanHats, script);
+}
+
 const std::vector<std::shared_ptr<Target>> &Engine::targets() const
 {
     return m_targets;
@@ -1442,6 +1450,9 @@ const std::vector<Script *> &Engine::getHats(Target *target, HatType type)
         case HatType::TargetClicked:
             return m_whenTargetClickedHats[target];
 
+        case HatType::WhenGreaterThan:
+            return m_whenGreaterThanHats[target];
+
         default: {
             static const std::vector<Script *> empty = {};
             return empty;
diff --git a/src/engine/internal/engine.h b/src/engine/internal/engine.h
index 84ef9508..7f48f631 100644
--- a/src/engine/internal/engine.h
+++ b/src/engine/internal/engine.h
@@ -125,6 +125,7 @@ class Engine : public IEngine
         void addCloneInitScript(std::shared_ptr<Block> hatBlock) override;
         void addKeyPressScript(std::shared_ptr<Block> hatBlock, int fieldId) override;
         void addTargetClickScript(std::shared_ptr<Block> hatBlock) override;
+        void addWhenGreaterThanScript(std::shared_ptr<Block> hatBlock) override;
 
         const std::vector<std::shared_ptr<Target>> &targets() const override;
         void setTargets(const std::vector<std::shared_ptr<Target>> &newTargets) override;
@@ -171,14 +172,16 @@ class Engine : public IEngine
             BackdropChanged,
             CloneInit,
             KeyPressed,
-            TargetClicked
+            TargetClicked,
+            WhenGreaterThan
         };
 
         enum class HatField
         {
             BroadcastOption,
             Backdrop,
-            KeyOption
+            KeyOption,
+            WhenGreaterThanMenu
         };
 
         std::vector<std::shared_ptr<VirtualMachine>> stepThreads();
@@ -240,6 +243,7 @@ class Engine : public IEngine
         std::unordered_map<Target *, std::vector<Script *>> m_cloneInitHats;
         std::unordered_map<Target *, std::vector<Script *>> m_whenKeyPressedHats;
         std::unordered_map<Target *, std::vector<Script *>> m_whenTargetClickedHats;
+        std::unordered_map<Target *, std::vector<Script *>> m_whenGreaterThanHats;
 
         std::unordered_map<Script *, std::unordered_map<HatField, int>> m_scriptHatFields; // HatField, field ID from the block implementation
 
diff --git a/test/blocks/event_blocks_test.cpp b/test/blocks/event_blocks_test.cpp
index 717930c8..8b2be2e8 100644
--- a/test/blocks/event_blocks_test.cpp
+++ b/test/blocks/event_blocks_test.cpp
@@ -7,6 +7,9 @@
 #include <scratchcpp/stage.h>
 #include <scratchcpp/costume.h>
 #include <enginemock.h>
+#include <audioinputmock.h>
+#include <audioloudnessmock.h>
+#include <timermock.h>
 
 #include "../common.h"
 #include "blocks/eventblocks.h"
@@ -42,6 +45,14 @@ class EventBlocksTest : public testing::Test
             block->addInput(input);
         }
 
+        void addValueInput(std::shared_ptr<Block> block, const std::string &name, EventBlocks::Inputs id, const Value &value) const
+        {
+            auto input = std::make_shared<Input>(name, Input::Type::Shadow);
+            input->setPrimaryValue(value);
+            input->setInputId(id);
+            block->addInput(input);
+        }
+
         void addObscuredInput(std::shared_ptr<Block> block, const std::string &name, EventBlocks::Inputs id, std::shared_ptr<Block> valueBlock) const
         {
             auto input = std::make_shared<Input>(name, Input::Type::ObscuredShadow);
@@ -50,10 +61,11 @@ class EventBlocksTest : public testing::Test
             block->addInput(input);
         }
 
-        void addValueField(std::shared_ptr<Block> block, const std::string &name, EventBlocks::Fields id, const std::string &value) const
+        void addValueField(std::shared_ptr<Block> block, const std::string &name, EventBlocks::Fields id, const std::string &value, int valueId = -1) const
         {
             auto field = std::make_shared<Field>(name, value);
             field->setFieldId(id);
+            field->setSpecialValueId(valueId);
             block->addField(field);
         }
 
@@ -90,16 +102,26 @@ TEST_F(EventBlocksTest, RegisterBlocks)
     EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "event_broadcastandwait", &EventBlocks::compileBroadcastAndWait));
     EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "event_whenbroadcastreceived", &EventBlocks::compileWhenBroadcastReceived));
     EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "event_whenbackdropswitchesto", &EventBlocks::compileWhenBackdropSwitchesTo));
+    EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "event_whengreaterthan", &EventBlocks::compileWhenGreaterThan));
     EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "event_whenkeypressed", &EventBlocks::compileWhenKeyPressed));
 
+    // Hat predicates
+    EXPECT_CALL(m_engineMock, addHatPredicateCompileFunction(m_section.get(), "event_whengreaterthan", &EventBlocks::compileWhenGreaterThanPredicate));
+
     // Inputs
-    EXPECT_CALL(m_engineMock, addInput(m_section.get(), "BROADCAST_INPUT", EventBlocks::BROADCAST_INPUT)).Times(1);
+    EXPECT_CALL(m_engineMock, addInput(m_section.get(), "BROADCAST_INPUT", EventBlocks::BROADCAST_INPUT));
+    EXPECT_CALL(m_engineMock, addInput(m_section.get(), "VALUE", EventBlocks::VALUE));
 
     // Fields
     EXPECT_CALL(m_engineMock, addField(m_section.get(), "BROADCAST_OPTION", EventBlocks::BROADCAST_OPTION));
     EXPECT_CALL(m_engineMock, addField(m_section.get(), "BACKDROP", EventBlocks::BACKDROP));
+    EXPECT_CALL(m_engineMock, addField(m_section.get(), "WHENGREATERTHANMENU", EventBlocks::WHENGREATERTHANMENU));
     EXPECT_CALL(m_engineMock, addField(m_section.get(), "KEY_OPTION", EventBlocks::KEY_OPTION));
 
+    // Field values
+    EXPECT_CALL(m_engineMock, addFieldValue(m_section.get(), "LOUDNESS", EventBlocks::Loudness));
+    EXPECT_CALL(m_engineMock, addFieldValue(m_section.get(), "TIMER", EventBlocks::Timer));
+
     m_section->registerBlocks(&m_engineMock);
 }
 
@@ -365,6 +387,134 @@ TEST_F(EventBlocksTest, WhenBackdropSwitchesTo)
     ASSERT_TRUE(compiler.lists().empty());
 }
 
+TEST_F(EventBlocksTest, WhenGreaterThanPredicate)
+{
+    Compiler compiler(&m_engineMock);
+
+    // when [loudness] > (8.9)
+    auto block1 = createEventBlock("a", "event_whengreaterthan");
+    addValueField(block1, "WHENGREATERTHANMENU", EventBlocks::WHENGREATERTHANMENU, "LOUDNESS", EventBlocks::Loudness);
+    addValueInput(block1, "VALUE", EventBlocks::VALUE, 8.9);
+
+    // when [timer] > (10.2)
+    auto block2 = createEventBlock("b", "event_whengreaterthan");
+    addValueField(block2, "WHENGREATERTHANMENU", EventBlocks::WHENGREATERTHANMENU, "TIMER", EventBlocks::Timer);
+    addValueInput(block2, "VALUE", EventBlocks::VALUE, 10.2);
+
+    // when [invalid value] > (0)
+    auto block3 = createEventBlock("c", "event_whengreaterthan");
+    addValueField(block3, "WHENGREATERTHANMENU", EventBlocks::WHENGREATERTHANMENU, "invalid value", -1);
+    addValueInput(block3, "VALUE", EventBlocks::VALUE, 0);
+
+    compiler.init();
+
+    EXPECT_CALL(m_engineMock, functionIndex(&EventBlocks::whenLoudnessGreaterThanPredicate)).WillOnce(Return(0));
+    EXPECT_CALL(m_engineMock, functionIndex(&EventBlocks::whenTimerGreaterThanPredicate)).Times(0);
+    compiler.setBlock(block1);
+    EventBlocks::compileWhenGreaterThanPredicate(&compiler);
+
+    EXPECT_CALL(m_engineMock, functionIndex(&EventBlocks::whenLoudnessGreaterThanPredicate)).Times(0);
+    EXPECT_CALL(m_engineMock, functionIndex(&EventBlocks::whenTimerGreaterThanPredicate)).WillOnce(Return(0));
+    compiler.setBlock(block2);
+    EventBlocks::compileWhenGreaterThanPredicate(&compiler);
+
+    EXPECT_CALL(m_engineMock, functionIndex(&EventBlocks::whenLoudnessGreaterThanPredicate)).Times(0);
+    EXPECT_CALL(m_engineMock, functionIndex(&EventBlocks::whenTimerGreaterThanPredicate)).Times(0);
+    compiler.setBlock(block3);
+    EventBlocks::compileWhenGreaterThanPredicate(&compiler);
+
+    compiler.end();
+
+    ASSERT_EQ(compiler.bytecode(), std::vector<unsigned int>({ vm::OP_START, vm::OP_CONST, 0, vm::OP_EXEC, 0, vm::OP_CONST, 1, vm::OP_EXEC, 0, vm::OP_NULL, vm::OP_HALT }));
+    ASSERT_EQ(compiler.constValues(), std::vector<Value>({ 8.9, 10.2 }));
+    ASSERT_TRUE(compiler.variables().empty());
+    ASSERT_TRUE(compiler.lists().empty());
+}
+
+TEST_F(EventBlocksTest, WhenGreaterThanPredicateImpl)
+{
+    static unsigned int bytecode1[] = { vm::OP_START, vm::OP_CONST, 0, vm::OP_EXEC, 0, vm::OP_HALT };
+    static unsigned int bytecode2[] = { vm::OP_START, vm::OP_CONST, 1, vm::OP_EXEC, 0, vm::OP_HALT };
+    static unsigned int bytecode3[] = { vm::OP_START, vm::OP_CONST, 2, vm::OP_EXEC, 0, vm::OP_HALT };
+    static unsigned int bytecode4[] = { vm::OP_START, vm::OP_CONST, 0, vm::OP_EXEC, 1, vm::OP_HALT };
+    static unsigned int bytecode5[] = { vm::OP_START, vm::OP_CONST, 1, vm::OP_EXEC, 1, vm::OP_HALT };
+    static unsigned int bytecode6[] = { vm::OP_START, vm::OP_CONST, 2, vm::OP_EXEC, 1, vm::OP_HALT };
+    static BlockFunc functions[] = { &EventBlocks::whenLoudnessGreaterThanPredicate, &EventBlocks::whenTimerGreaterThanPredicate };
+    static Value constValues[] = { 22.7, 23, 23.5 };
+
+    VirtualMachine vm(nullptr, &m_engineMock, nullptr);
+    vm.setFunctions(functions);
+    vm.setConstValues(constValues);
+
+    AudioInputMock audioInput;
+    auto audioLoudness = std::make_shared<AudioLoudnessMock>();
+    EventBlocks::audioInput = &audioInput;
+    EXPECT_CALL(audioInput, audioLoudness()).Times(3).WillRepeatedly(Return(audioLoudness));
+    EXPECT_CALL(*audioLoudness, getLoudness()).Times(3).WillRepeatedly(Return(23));
+
+    vm.setBytecode(bytecode1);
+    vm.run();
+    ASSERT_EQ(vm.registerCount(), 1);
+    ASSERT_TRUE(vm.getInput(0, 1)->toBool());
+
+    vm.reset();
+    vm.setBytecode(bytecode2);
+    vm.run();
+    ASSERT_EQ(vm.registerCount(), 1);
+    ASSERT_FALSE(vm.getInput(0, 1)->toBool());
+
+    vm.reset();
+    vm.setBytecode(bytecode3);
+    vm.run();
+    ASSERT_EQ(vm.registerCount(), 1);
+    ASSERT_FALSE(vm.getInput(0, 1)->toBool());
+
+    EventBlocks::audioInput = nullptr;
+
+    TimerMock timer;
+    EXPECT_CALL(m_engineMock, timer()).Times(3).WillRepeatedly(Return(&timer));
+    EXPECT_CALL(timer, value()).Times(3).WillRepeatedly(Return(23));
+
+    vm.reset();
+    vm.setBytecode(bytecode4);
+    vm.run();
+    ASSERT_EQ(vm.registerCount(), 1);
+    ASSERT_TRUE(vm.getInput(0, 1)->toBool());
+
+    vm.reset();
+    vm.setBytecode(bytecode5);
+    vm.run();
+    ASSERT_EQ(vm.registerCount(), 1);
+    ASSERT_FALSE(vm.getInput(0, 1)->toBool());
+
+    vm.reset();
+    vm.setBytecode(bytecode6);
+    vm.run();
+    ASSERT_EQ(vm.registerCount(), 1);
+    ASSERT_FALSE(vm.getInput(0, 1)->toBool());
+}
+
+TEST_F(EventBlocksTest, WhenGreaterThan)
+{
+    Compiler compiler(&m_engineMock);
+
+    // when [loudness] > (8.9)
+    auto block = createEventBlock("a", "event_whengreaterthan");
+    addValueField(block, "WHENGREATERTHANMENU", EventBlocks::WHENGREATERTHANMENU, "LOUDNESS", EventBlocks::Loudness);
+    addValueInput(block, "VALUE", EventBlocks::VALUE, 8.9);
+
+    EXPECT_CALL(m_engineMock, addWhenGreaterThanScript(block));
+    compiler.init();
+    compiler.setBlock(block);
+    EventBlocks::compileWhenGreaterThan(&compiler);
+    compiler.end();
+
+    ASSERT_EQ(compiler.bytecode(), std::vector<unsigned int>({ vm::OP_START, vm::OP_HALT }));
+    ASSERT_TRUE(compiler.constValues().empty());
+    ASSERT_TRUE(compiler.variables().empty());
+    ASSERT_TRUE(compiler.lists().empty());
+}
+
 TEST_F(EventBlocksTest, WhenKeyPressed)
 {
     Compiler compiler(&m_engineMock);
diff --git a/test/engine/engine_test.cpp b/test/engine/engine_test.cpp
index 7cd6e32a..e4c59e7a 100644
--- a/test/engine/engine_test.cpp
+++ b/test/engine/engine_test.cpp
@@ -1792,6 +1792,37 @@ TEST(EngineTest, StopOtherScriptsInSprite)
     ASSERT_EQ(GET_VAR(stage, "l")->value().toInt(), 110);
 }
 
+TEST(EngineTest, EdgeActivatedHats)
+{
+    Project p("when_greater_than.sb3");
+    ASSERT_TRUE(p.load());
+    p.run();
+
+    auto engine = std::static_pointer_cast<Engine>(p.engine());
+
+    Stage *stage = engine->stage();
+    ASSERT_TRUE(stage);
+
+    ASSERT_VAR(stage, "test");
+    auto var = GET_VAR(stage, "test");
+    ASSERT_EQ(var->value().toInt(), 0);
+
+    TimerMock timer;
+    engine->setTimer(&timer);
+
+    EXPECT_CALL(timer, value()).WillOnce(Return(5));
+    engine->step();
+    ASSERT_EQ(var->value().toInt(), 0);
+
+    EXPECT_CALL(timer, value()).WillOnce(Return(10));
+    engine->step();
+    ASSERT_EQ(var->value().toInt(), 0);
+
+    EXPECT_CALL(timer, value()).WillOnce(Return(10.2));
+    engine->step();
+    ASSERT_EQ(var->value().toInt(), 1);
+}
+
 TEST(EngineTest, UserAgent)
 {
     Engine engine;
diff --git a/test/mocks/enginemock.h b/test/mocks/enginemock.h
index d744c497..e0e76d5a 100644
--- a/test/mocks/enginemock.h
+++ b/test/mocks/enginemock.h
@@ -106,6 +106,7 @@ class EngineMock : public IEngine
         MOCK_METHOD(void, addCloneInitScript, (std::shared_ptr<Block>), (override));
         MOCK_METHOD(void, addKeyPressScript, (std::shared_ptr<Block>, int), (override));
         MOCK_METHOD(void, addTargetClickScript, (std::shared_ptr<Block>), (override));
+        MOCK_METHOD(void, addWhenGreaterThanScript, (std::shared_ptr<Block>), (override));
 
         MOCK_METHOD(const std::vector<std::shared_ptr<Target>> &, targets, (), (const, override));
         MOCK_METHOD(void, setTargets, (const std::vector<std::shared_ptr<Target>> &), (override));
diff --git a/test/when_greater_than.sb3 b/test/when_greater_than.sb3
new file mode 100644
index 0000000000000000000000000000000000000000..edb9e1dab5b7fe78ba842d8686bd757e460ff49d
GIT binary patch

From 20adf83207eaed727e818341939f0906761ffedc Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Sat, 16 Mar 2024 17:44:56 +0100
Subject: [PATCH 31/77] README: Mark event blocks as finished

---
 README.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/README.md b/README.md
index f945d5a9..fe4b4a3d 100644
--- a/README.md
+++ b/README.md
@@ -125,7 +125,7 @@ libscratchcpp::Project p("https://scratch.mit.edu/projects/XXXXXX");
 - [x] Motion blocks
 - [x] Looks blocks
 - [ ] Sound blocks
-- [ ] Event blocks
+- [x] Event blocks
 - [x] Control blocks
 - [ ] Sensing blocks
 - [x] Operator blocks

From 0afb4efadca624bed5a230f2b04e2c3510c6580e Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Sun, 17 Mar 2024 11:21:45 +0100
Subject: [PATCH 32/77] Replace TestSection with BlockSectionMock

---
 test/engine/CMakeLists.txt    |  2 --
 test/engine/engine_test.cpp   | 56 ++++++++++++++++++++++-------------
 test/engine/testsection.cpp   | 14 ---------
 test/engine/testsection.h     | 16 ----------
 test/mocks/blocksectionmock.h | 15 ++++++++++
 5 files changed, 51 insertions(+), 52 deletions(-)
 delete mode 100644 test/engine/testsection.cpp
 delete mode 100644 test/engine/testsection.h
 create mode 100644 test/mocks/blocksectionmock.h

diff --git a/test/engine/CMakeLists.txt b/test/engine/CMakeLists.txt
index b8d97c5c..d669bda3 100644
--- a/test/engine/CMakeLists.txt
+++ b/test/engine/CMakeLists.txt
@@ -1,8 +1,6 @@
 add_executable(
   engine_test
   engine_test.cpp
-  testsection.cpp
-  testsection.h
 )
 
 target_link_libraries(
diff --git a/test/engine/engine_test.cpp b/test/engine/engine_test.cpp
index e4c59e7a..4ca5a801 100644
--- a/test/engine/engine_test.cpp
+++ b/test/engine/engine_test.cpp
@@ -17,10 +17,10 @@
 #include <audiooutputmock.h>
 #include <audioplayermock.h>
 #include <monitorhandlermock.h>
+#include <blocksectionmock.h>
 #include <thread>
 
 #include "../common.h"
-#include "testsection.h"
 #include "engine/internal/engine.h"
 #include "engine/internal/clock.h"
 
@@ -77,7 +77,8 @@ TEST(EngineTest, Clear)
     auto broadcast2 = std::make_shared<Broadcast>("", "");
     engine.setBroadcasts({ broadcast1, broadcast2 });
 
-    auto section = std::make_shared<TestSection>();
+    auto section = std::make_shared<BlockSectionMock>();
+    EXPECT_CALL(*section, registerBlocks);
     engine.registerSection(section);
 
     auto monitor1 = std::make_shared<Monitor>("", "");
@@ -126,7 +127,8 @@ TEST(EngineTest, CompileAndExecuteMonitors)
     m2->setSprite(sprite.get());
     engine.setMonitors({ m1, m2 });
 
-    auto section = std::make_shared<TestSection>();
+    auto section = std::make_shared<BlockSectionMock>();
+    EXPECT_CALL(*section, registerBlocks);
     engine.registerSection(section);
     engine.addCompileFunction(section.get(), m1->opcode(), [](Compiler *compiler) { compiler->addConstValue(5.4); });
     engine.addCompileFunction(section.get(), m2->opcode(), [](Compiler *compiler) { compiler->addConstValue("test"); });
@@ -859,12 +861,16 @@ TEST(EngineTest, Sections)
 {
     Engine engine;
 
-    auto section1 = std::make_shared<TestSection>();
+    auto section1 = std::make_shared<BlockSectionMock>();
+    EXPECT_CALL(*section1, registerBlocks(&engine));
     engine.registerSection(section1);
 
-    auto section2 = std::make_shared<TestSection>();
+    auto section2 = std::make_shared<BlockSectionMock>();
+    EXPECT_CALL(*section2, registerBlocks(&engine));
     engine.registerSection(section2);
 
+    EXPECT_CALL(*section1, name()).WillOnce(Return("test"));
+    EXPECT_CALL(*section1, registerBlocks).Times(0);
     engine.registerSection(section1); // register existing section
 
     ASSERT_EQ(engine.registeredSections().size(), 2);
@@ -908,15 +914,17 @@ TEST(EngineTest, CompileFunctions)
 {
     Engine engine;
 
-    auto section1 = std::make_shared<TestSection>();
+    auto section1 = std::make_shared<BlockSectionMock>();
+    EXPECT_CALL(*section1, registerBlocks);
     engine.registerSection(section1);
     auto container1 = engine.blockSectionContainer(section1.get());
 
-    auto section2 = std::make_shared<TestSection>();
+    auto section2 = std::make_shared<BlockSectionMock>();
+    EXPECT_CALL(*section2, registerBlocks);
     engine.registerSection(section2);
     auto container2 = engine.blockSectionContainer(section2.get());
 
-    TestSection section3;
+    BlockSectionMock section3;
 
     engine.addCompileFunction(section1.get(), "test1", &compileTest1);
     engine.addCompileFunction(section2.get(), "test2", &compileTest2);
@@ -933,15 +941,17 @@ TEST(EngineTest, HatBlocks)
 {
     Engine engine;
 
-    auto section1 = std::make_shared<TestSection>();
+    auto section1 = std::make_shared<BlockSectionMock>();
+    EXPECT_CALL(*section1, registerBlocks);
     engine.registerSection(section1);
     auto container1 = engine.blockSectionContainer(section1.get());
 
-    auto section2 = std::make_shared<TestSection>();
+    auto section2 = std::make_shared<BlockSectionMock>();
+    EXPECT_CALL(*section2, registerBlocks);
     engine.registerSection(section2);
     auto container2 = engine.blockSectionContainer(section2.get());
 
-    TestSection section3;
+    BlockSectionMock section3;
 
     engine.addHatBlock(section1.get(), "test1");
     engine.addHatBlock(section2.get(), "test2");
@@ -958,15 +968,17 @@ TEST(EngineTest, Inputs)
 {
     Engine engine;
 
-    auto section1 = std::make_shared<TestSection>();
+    auto section1 = std::make_shared<BlockSectionMock>();
+    EXPECT_CALL(*section1, registerBlocks);
     engine.registerSection(section1);
     auto container1 = engine.blockSectionContainer(section1.get());
 
-    auto section2 = std::make_shared<TestSection>();
+    auto section2 = std::make_shared<BlockSectionMock>();
+    EXPECT_CALL(*section2, registerBlocks);
     engine.registerSection(section2);
     auto container2 = engine.blockSectionContainer(section2.get());
 
-    TestSection section3;
+    BlockSectionMock section3;
 
     engine.addInput(section1.get(), "VALUE1", 1);
     engine.addInput(section2.get(), "VALUE2", 2);
@@ -985,15 +997,17 @@ TEST(EngineTest, Fields)
 {
     Engine engine;
 
-    auto section1 = std::make_shared<TestSection>();
+    auto section1 = std::make_shared<BlockSectionMock>();
+    EXPECT_CALL(*section1, registerBlocks);
     engine.registerSection(section1);
     auto container1 = engine.blockSectionContainer(section1.get());
 
-    auto section2 = std::make_shared<TestSection>();
+    auto section2 = std::make_shared<BlockSectionMock>();
+    EXPECT_CALL(*section2, registerBlocks);
     engine.registerSection(section2);
     auto container2 = engine.blockSectionContainer(section2.get());
 
-    TestSection section3;
+    BlockSectionMock section3;
 
     engine.addField(section1.get(), "VALUE1", 1);
     engine.addField(section2.get(), "VALUE2", 2);
@@ -1012,15 +1026,17 @@ TEST(EngineTest, FieldValues)
 {
     Engine engine;
 
-    auto section1 = std::make_shared<TestSection>();
+    auto section1 = std::make_shared<BlockSectionMock>();
+    EXPECT_CALL(*section1, registerBlocks);
     engine.registerSection(section1);
     auto container1 = engine.blockSectionContainer(section1.get());
 
-    auto section2 = std::make_shared<TestSection>();
+    auto section2 = std::make_shared<BlockSectionMock>();
+    EXPECT_CALL(*section2, registerBlocks);
     engine.registerSection(section2);
     auto container2 = engine.blockSectionContainer(section2.get());
 
-    TestSection section3;
+    BlockSectionMock section3;
 
     engine.addFieldValue(section1.get(), "value1", 1);
     engine.addFieldValue(section2.get(), "value2", 2);
diff --git a/test/engine/testsection.cpp b/test/engine/testsection.cpp
deleted file mode 100644
index 255e6938..00000000
--- a/test/engine/testsection.cpp
+++ /dev/null
@@ -1,14 +0,0 @@
-#include <scratchcpp/iengine.h>
-
-#include "testsection.h"
-
-using namespace libscratchcpp;
-
-std::string TestSection::name() const
-{
-    return "Test";
-}
-
-void TestSection::registerBlocks(IEngine *engine)
-{
-}
diff --git a/test/engine/testsection.h b/test/engine/testsection.h
deleted file mode 100644
index 78a0507d..00000000
--- a/test/engine/testsection.h
+++ /dev/null
@@ -1,16 +0,0 @@
-#pragma once
-
-#include <scratchcpp/iblocksection.h>
-
-namespace libscratchcpp
-{
-
-class TestSection : public IBlockSection
-{
-    public:
-        std::string name() const override;
-
-        void registerBlocks(IEngine *engine) override;
-};
-
-} // namespace libscratchcpp
diff --git a/test/mocks/blocksectionmock.h b/test/mocks/blocksectionmock.h
new file mode 100644
index 00000000..2de5f41d
--- /dev/null
+++ b/test/mocks/blocksectionmock.h
@@ -0,0 +1,15 @@
+#pragma once
+
+#include <scratchcpp/iblocksection.h>
+#include <gmock/gmock.h>
+
+using namespace libscratchcpp;
+
+class BlockSectionMock : public IBlockSection
+{
+    public:
+        MOCK_METHOD(std::string, name, (), (const, override));
+        MOCK_METHOD(bool, categoryVisible, (), (const, override));
+
+        MOCK_METHOD(void, registerBlocks, (IEngine *), (override));
+};

From aa50fad1ff3d34f3bd0b4887fb8d15693cf03b49 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Sun, 17 Mar 2024 11:34:00 +0100
Subject: [PATCH 33/77] Add missing tests for block section containers

---
 test/engine/engine_test.cpp | 96 ++++++++++++++++++++++++++++++++++++-
 1 file changed, 95 insertions(+), 1 deletion(-)

diff --git a/test/engine/engine_test.cpp b/test/engine/engine_test.cpp
index 4ca5a801..9cc7f871 100644
--- a/test/engine/engine_test.cpp
+++ b/test/engine/engine_test.cpp
@@ -937,6 +937,100 @@ TEST(EngineTest, CompileFunctions)
     ASSERT_EQ(container2->resolveBlockCompileFunc("test2"), &compileTest2);
 }
 
+TEST(EngineTest, HatPredicateCompileFunctions)
+{
+    Engine engine;
+
+    auto section1 = std::make_shared<BlockSectionMock>();
+    EXPECT_CALL(*section1, registerBlocks);
+    engine.registerSection(section1);
+    auto container1 = engine.blockSectionContainer(section1.get());
+
+    auto section2 = std::make_shared<BlockSectionMock>();
+    EXPECT_CALL(*section2, registerBlocks);
+    engine.registerSection(section2);
+    auto container2 = engine.blockSectionContainer(section2.get());
+
+    BlockSectionMock section3;
+
+    engine.addHatPredicateCompileFunction(section1.get(), "test1", &compileTest1);
+    engine.addHatPredicateCompileFunction(section2.get(), "test2", &compileTest2);
+    engine.addHatPredicateCompileFunction(section1.get(), "test1", &compileTest1);
+    engine.addHatPredicateCompileFunction(&section3, "test1", &compileTest1);
+
+    ASSERT_EQ(container1->resolveHatPredicateCompileFunc("test1"), &compileTest1);
+    ASSERT_EQ(container1->resolveHatPredicateCompileFunc("test2"), nullptr);
+    ASSERT_EQ(container2->resolveHatPredicateCompileFunc("test1"), nullptr);
+    ASSERT_EQ(container2->resolveHatPredicateCompileFunc("test2"), &compileTest2);
+}
+
+TEST(EngineTest, MonitorNameFunctions)
+{
+    Engine engine;
+
+    auto section1 = std::make_shared<BlockSectionMock>();
+    EXPECT_CALL(*section1, registerBlocks);
+    engine.registerSection(section1);
+    auto container1 = engine.blockSectionContainer(section1.get());
+
+    auto section2 = std::make_shared<BlockSectionMock>();
+    EXPECT_CALL(*section2, registerBlocks);
+    engine.registerSection(section2);
+    auto container2 = engine.blockSectionContainer(section2.get());
+
+    BlockSectionMock section3;
+
+    MonitorNameFunc f1 = [](Block *) -> const std::string & {
+        static const std::string ret;
+        return ret;
+    };
+
+    MonitorNameFunc f2 = [](Block *) -> const std::string & {
+        static const std::string ret;
+        return ret;
+    };
+
+    engine.addMonitorNameFunction(section1.get(), "test1", f1);
+    engine.addMonitorNameFunction(section2.get(), "test2", f2);
+    engine.addMonitorNameFunction(section1.get(), "test1", f1);
+    engine.addMonitorNameFunction(&section3, "test1", f1);
+
+    ASSERT_EQ(container1->resolveMonitorNameFunc("test1"), f1);
+    ASSERT_EQ(container1->resolveMonitorNameFunc("test2"), nullptr);
+    ASSERT_EQ(container2->resolveMonitorNameFunc("test1"), nullptr);
+    ASSERT_EQ(container2->resolveMonitorNameFunc("test2"), f2);
+}
+
+TEST(EngineTest, MonitorChangeFunctions)
+{
+    Engine engine;
+
+    auto section1 = std::make_shared<BlockSectionMock>();
+    EXPECT_CALL(*section1, registerBlocks);
+    engine.registerSection(section1);
+    auto container1 = engine.blockSectionContainer(section1.get());
+
+    auto section2 = std::make_shared<BlockSectionMock>();
+    EXPECT_CALL(*section2, registerBlocks);
+    engine.registerSection(section2);
+    auto container2 = engine.blockSectionContainer(section2.get());
+
+    BlockSectionMock section3;
+
+    MonitorChangeFunc f1 = [](Block *, const Value &) {};
+    MonitorChangeFunc f2 = [](Block *, const Value &) {};
+
+    engine.addMonitorChangeFunction(section1.get(), "test1", f1);
+    engine.addMonitorChangeFunction(section2.get(), "test2", f2);
+    engine.addMonitorChangeFunction(section1.get(), "test1", f1);
+    engine.addMonitorChangeFunction(&section3, "test1", f1);
+
+    ASSERT_EQ(container1->resolveMonitorChangeFunc("test1"), f1);
+    ASSERT_EQ(container1->resolveMonitorChangeFunc("test2"), nullptr);
+    ASSERT_EQ(container2->resolveMonitorChangeFunc("test1"), nullptr);
+    ASSERT_EQ(container2->resolveMonitorChangeFunc("test2"), f2);
+}
+
 TEST(EngineTest, HatBlocks)
 {
     Engine engine;
@@ -1040,7 +1134,7 @@ TEST(EngineTest, FieldValues)
 
     engine.addFieldValue(section1.get(), "value1", 1);
     engine.addFieldValue(section2.get(), "value2", 2);
-    engine.addFieldValue(section1.get(), "value1", 3); // change ID of existing field
+    engine.addFieldValue(section1.get(), "value1", 3); // change ID of existing field value
     engine.addFieldValue(&section3, "value3", 4);
 
     ASSERT_EQ(container1->resolveFieldValue("value1"), 3);

From 3f4c47d751595a2e5f29c14d86bd8c4620d75655 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Sun, 17 Mar 2024 11:37:53 +0100
Subject: [PATCH 34/77] Add onInit() method to IBlockSection

---
 include/scratchcpp/iblocksection.h |  3 +++
 src/engine/internal/engine.cpp     |  1 +
 test/engine/engine_test.cpp        | 21 +++++++++++++++++++++
 test/mocks/blocksectionmock.h      |  1 +
 4 files changed, 26 insertions(+)

diff --git a/include/scratchcpp/iblocksection.h b/include/scratchcpp/iblocksection.h
index d12159e4..92b71cfb 100644
--- a/include/scratchcpp/iblocksection.h
+++ b/include/scratchcpp/iblocksection.h
@@ -36,6 +36,9 @@ class LIBSCRATCHCPP_EXPORT IBlockSection
 
         /*! Override this method to register blocks. */
         virtual void registerBlocks(IEngine *engine) = 0;
+
+        /*! This method is called when a project is loaded. */
+        virtual void onInit(IEngine *engine) { }
 };
 
 } // namespace libscratchcpp
diff --git a/src/engine/internal/engine.cpp b/src/engine/internal/engine.cpp
index a810a99d..ed4ce7ef 100644
--- a/src/engine/internal/engine.cpp
+++ b/src/engine/internal/engine.cpp
@@ -864,6 +864,7 @@ void Engine::registerSection(std::shared_ptr<IBlockSection> section)
 
         m_sections[section] = std::make_unique<BlockSectionContainer>();
         section->registerBlocks(this);
+        section->onInit(this);
     }
 }
 
diff --git a/test/engine/engine_test.cpp b/test/engine/engine_test.cpp
index 9cc7f871..b2d390fc 100644
--- a/test/engine/engine_test.cpp
+++ b/test/engine/engine_test.cpp
@@ -79,6 +79,7 @@ TEST(EngineTest, Clear)
 
     auto section = std::make_shared<BlockSectionMock>();
     EXPECT_CALL(*section, registerBlocks);
+    EXPECT_CALL(*section, onInit);
     engine.registerSection(section);
 
     auto monitor1 = std::make_shared<Monitor>("", "");
@@ -129,6 +130,7 @@ TEST(EngineTest, CompileAndExecuteMonitors)
 
     auto section = std::make_shared<BlockSectionMock>();
     EXPECT_CALL(*section, registerBlocks);
+    EXPECT_CALL(*section, onInit);
     engine.registerSection(section);
     engine.addCompileFunction(section.get(), m1->opcode(), [](Compiler *compiler) { compiler->addConstValue(5.4); });
     engine.addCompileFunction(section.get(), m2->opcode(), [](Compiler *compiler) { compiler->addConstValue("test"); });
@@ -863,14 +865,17 @@ TEST(EngineTest, Sections)
 
     auto section1 = std::make_shared<BlockSectionMock>();
     EXPECT_CALL(*section1, registerBlocks(&engine));
+    EXPECT_CALL(*section1, onInit(&engine));
     engine.registerSection(section1);
 
     auto section2 = std::make_shared<BlockSectionMock>();
     EXPECT_CALL(*section2, registerBlocks(&engine));
+    EXPECT_CALL(*section2, onInit(&engine));
     engine.registerSection(section2);
 
     EXPECT_CALL(*section1, name()).WillOnce(Return("test"));
     EXPECT_CALL(*section1, registerBlocks).Times(0);
+    EXPECT_CALL(*section1, onInit).Times(0);
     engine.registerSection(section1); // register existing section
 
     ASSERT_EQ(engine.registeredSections().size(), 2);
@@ -916,11 +921,13 @@ TEST(EngineTest, CompileFunctions)
 
     auto section1 = std::make_shared<BlockSectionMock>();
     EXPECT_CALL(*section1, registerBlocks);
+    EXPECT_CALL(*section1, onInit);
     engine.registerSection(section1);
     auto container1 = engine.blockSectionContainer(section1.get());
 
     auto section2 = std::make_shared<BlockSectionMock>();
     EXPECT_CALL(*section2, registerBlocks);
+    EXPECT_CALL(*section2, onInit);
     engine.registerSection(section2);
     auto container2 = engine.blockSectionContainer(section2.get());
 
@@ -943,11 +950,13 @@ TEST(EngineTest, HatPredicateCompileFunctions)
 
     auto section1 = std::make_shared<BlockSectionMock>();
     EXPECT_CALL(*section1, registerBlocks);
+    EXPECT_CALL(*section1, onInit);
     engine.registerSection(section1);
     auto container1 = engine.blockSectionContainer(section1.get());
 
     auto section2 = std::make_shared<BlockSectionMock>();
     EXPECT_CALL(*section2, registerBlocks);
+    EXPECT_CALL(*section2, onInit);
     engine.registerSection(section2);
     auto container2 = engine.blockSectionContainer(section2.get());
 
@@ -970,11 +979,13 @@ TEST(EngineTest, MonitorNameFunctions)
 
     auto section1 = std::make_shared<BlockSectionMock>();
     EXPECT_CALL(*section1, registerBlocks);
+    EXPECT_CALL(*section1, onInit);
     engine.registerSection(section1);
     auto container1 = engine.blockSectionContainer(section1.get());
 
     auto section2 = std::make_shared<BlockSectionMock>();
     EXPECT_CALL(*section2, registerBlocks);
+    EXPECT_CALL(*section2, onInit);
     engine.registerSection(section2);
     auto container2 = engine.blockSectionContainer(section2.get());
 
@@ -1007,11 +1018,13 @@ TEST(EngineTest, MonitorChangeFunctions)
 
     auto section1 = std::make_shared<BlockSectionMock>();
     EXPECT_CALL(*section1, registerBlocks);
+    EXPECT_CALL(*section1, onInit);
     engine.registerSection(section1);
     auto container1 = engine.blockSectionContainer(section1.get());
 
     auto section2 = std::make_shared<BlockSectionMock>();
     EXPECT_CALL(*section2, registerBlocks);
+    EXPECT_CALL(*section2, onInit);
     engine.registerSection(section2);
     auto container2 = engine.blockSectionContainer(section2.get());
 
@@ -1037,11 +1050,13 @@ TEST(EngineTest, HatBlocks)
 
     auto section1 = std::make_shared<BlockSectionMock>();
     EXPECT_CALL(*section1, registerBlocks);
+    EXPECT_CALL(*section1, onInit);
     engine.registerSection(section1);
     auto container1 = engine.blockSectionContainer(section1.get());
 
     auto section2 = std::make_shared<BlockSectionMock>();
     EXPECT_CALL(*section2, registerBlocks);
+    EXPECT_CALL(*section2, onInit);
     engine.registerSection(section2);
     auto container2 = engine.blockSectionContainer(section2.get());
 
@@ -1064,11 +1079,13 @@ TEST(EngineTest, Inputs)
 
     auto section1 = std::make_shared<BlockSectionMock>();
     EXPECT_CALL(*section1, registerBlocks);
+    EXPECT_CALL(*section1, onInit);
     engine.registerSection(section1);
     auto container1 = engine.blockSectionContainer(section1.get());
 
     auto section2 = std::make_shared<BlockSectionMock>();
     EXPECT_CALL(*section2, registerBlocks);
+    EXPECT_CALL(*section2, onInit);
     engine.registerSection(section2);
     auto container2 = engine.blockSectionContainer(section2.get());
 
@@ -1093,11 +1110,13 @@ TEST(EngineTest, Fields)
 
     auto section1 = std::make_shared<BlockSectionMock>();
     EXPECT_CALL(*section1, registerBlocks);
+    EXPECT_CALL(*section1, onInit);
     engine.registerSection(section1);
     auto container1 = engine.blockSectionContainer(section1.get());
 
     auto section2 = std::make_shared<BlockSectionMock>();
     EXPECT_CALL(*section2, registerBlocks);
+    EXPECT_CALL(*section2, onInit);
     engine.registerSection(section2);
     auto container2 = engine.blockSectionContainer(section2.get());
 
@@ -1122,11 +1141,13 @@ TEST(EngineTest, FieldValues)
 
     auto section1 = std::make_shared<BlockSectionMock>();
     EXPECT_CALL(*section1, registerBlocks);
+    EXPECT_CALL(*section1, onInit);
     engine.registerSection(section1);
     auto container1 = engine.blockSectionContainer(section1.get());
 
     auto section2 = std::make_shared<BlockSectionMock>();
     EXPECT_CALL(*section2, registerBlocks);
+    EXPECT_CALL(*section2, onInit);
     engine.registerSection(section2);
     auto container2 = engine.blockSectionContainer(section2.get());
 
diff --git a/test/mocks/blocksectionmock.h b/test/mocks/blocksectionmock.h
index 2de5f41d..5050d83c 100644
--- a/test/mocks/blocksectionmock.h
+++ b/test/mocks/blocksectionmock.h
@@ -12,4 +12,5 @@ class BlockSectionMock : public IBlockSection
         MOCK_METHOD(bool, categoryVisible, (), (const, override));
 
         MOCK_METHOD(void, registerBlocks, (IEngine *), (override));
+        MOCK_METHOD(void, onInit, (IEngine *), (override));
 };

From 3fd4cc90eeb976c2a55410e1323686b297be5044 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Sun, 17 Mar 2024 12:01:46 +0100
Subject: [PATCH 35/77] Clear waiting sounds on init

---
 src/blocks/soundblocks.cpp | 6 ++++++
 src/blocks/soundblocks.h   | 1 +
 2 files changed, 7 insertions(+)

diff --git a/src/blocks/soundblocks.cpp b/src/blocks/soundblocks.cpp
index edd9659b..4543f6c3 100644
--- a/src/blocks/soundblocks.cpp
+++ b/src/blocks/soundblocks.cpp
@@ -33,6 +33,12 @@ void SoundBlocks::registerBlocks(IEngine *engine)
     engine->addInput(this, "VOLUME", VOLUME);
 }
 
+void SoundBlocks::onInit(IEngine *engine)
+{
+    m_waitingSounds.clear();
+    // TODO: Remove stopped threads from m_waitingSounds
+}
+
 bool SoundBlocks::compilePlayCommon(Compiler *compiler, bool untilDone, bool *byIndex)
 {
     Target *target = compiler->target();
diff --git a/src/blocks/soundblocks.h b/src/blocks/soundblocks.h
index f2a1faed..866be118 100644
--- a/src/blocks/soundblocks.h
+++ b/src/blocks/soundblocks.h
@@ -36,6 +36,7 @@ class SoundBlocks : public IBlockSection
         std::string name() const override;
 
         void registerBlocks(IEngine *engine) override;
+        void onInit(IEngine *engine) override;
 
         static bool compilePlayCommon(Compiler *compiler, bool untilDone, bool *byIndex = nullptr);
         static void compilePlay(Compiler *compiler);

From b1c63d62383225d5326bc119a42901846f2545dc Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Sun, 17 Mar 2024 12:42:26 +0100
Subject: [PATCH 36/77] Add a getter for block functions to IEngine

---
 include/scratchcpp/iengine.h   | 3 +++
 src/engine/internal/engine.cpp | 5 +++++
 src/engine/internal/engine.h   | 1 +
 test/engine/engine_test.cpp    | 2 ++
 test/mocks/enginemock.h        | 1 +
 5 files changed, 12 insertions(+)

diff --git a/include/scratchcpp/iengine.h b/include/scratchcpp/iengine.h
index 9e559420..987fba22 100644
--- a/include/scratchcpp/iengine.h
+++ b/include/scratchcpp/iengine.h
@@ -217,6 +217,9 @@ class LIBSCRATCHCPP_EXPORT IEngine
         /*! Returns the index of the given block function. */
         virtual unsigned int functionIndex(BlockFunc f) = 0;
 
+        /*! Returns the list of block functions. */
+        virtual const std::vector<BlockFunc> &blockFunctions() const = 0;
+
         /*!
          * Call this from IBlockSection#registerBlocks() to add a compile function to a block section.
          * \see <a href="blockSections.html">Block sections</a>
diff --git a/src/engine/internal/engine.cpp b/src/engine/internal/engine.cpp
index ed4ce7ef..85a21727 100644
--- a/src/engine/internal/engine.cpp
+++ b/src/engine/internal/engine.cpp
@@ -887,6 +887,11 @@ unsigned int Engine::functionIndex(BlockFunc f)
     return m_functions.size() - 1;
 }
 
+const std::vector<BlockFunc> &Engine::blockFunctions() const
+{
+    return m_functions;
+}
+
 void Engine::addCompileFunction(IBlockSection *section, const std::string &opcode, BlockComp f)
 {
     auto container = blockSectionContainer(section);
diff --git a/src/engine/internal/engine.h b/src/engine/internal/engine.h
index 7f48f631..44ac44fb 100644
--- a/src/engine/internal/engine.h
+++ b/src/engine/internal/engine.h
@@ -103,6 +103,7 @@ class Engine : public IEngine
         void registerSection(std::shared_ptr<IBlockSection> section) override;
         std::vector<std::shared_ptr<IBlockSection>> registeredSections() const;
         unsigned int functionIndex(BlockFunc f) override;
+        const std::vector<BlockFunc> &blockFunctions() const override;
 
         void addCompileFunction(IBlockSection *section, const std::string &opcode, BlockComp f) override;
         void addHatPredicateCompileFunction(IBlockSection *section, const std::string &opcode, HatPredicateCompileFunc f) override;
diff --git a/test/engine/engine_test.cpp b/test/engine/engine_test.cpp
index b2d390fc..1929c7a3 100644
--- a/test/engine/engine_test.cpp
+++ b/test/engine/engine_test.cpp
@@ -905,6 +905,8 @@ TEST(EngineTest, Functions)
     ASSERT_EQ(engine.functionIndex(&testFunction2), 1);
     ASSERT_EQ(engine.functionIndex(&testFunction1), 0);
     ASSERT_EQ(engine.functionIndex(&testFunction2), 1);
+
+    ASSERT_EQ(engine.blockFunctions(), std::vector<BlockFunc>({ &testFunction1, &testFunction2 }));
 }
 
 void compileTest1(Compiler *)
diff --git a/test/mocks/enginemock.h b/test/mocks/enginemock.h
index e0e76d5a..be7a2aa3 100644
--- a/test/mocks/enginemock.h
+++ b/test/mocks/enginemock.h
@@ -84,6 +84,7 @@ class EngineMock : public IEngine
 
         MOCK_METHOD(void, registerSection, (std::shared_ptr<IBlockSection>), (override));
         MOCK_METHOD(unsigned int, functionIndex, (BlockFunc), (override));
+        MOCK_METHOD(const std::vector<BlockFunc> &, blockFunctions, (), (const, override));
 
         MOCK_METHOD(void, addCompileFunction, (IBlockSection *, const std::string &, BlockComp), (override));
         MOCK_METHOD(void, addHatPredicateCompileFunction, (IBlockSection *, const std::string &, HatPredicateCompileFunc), (override));

From 71b2b9e23393b9ddc03288c4d06500d328604d21 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Sun, 17 Mar 2024 12:43:18 +0100
Subject: [PATCH 37/77] Get functions from engine in Script

---
 include/scratchcpp/script.h    |  3 ++-
 src/engine/internal/engine.cpp |  2 --
 src/engine/script.cpp          | 24 +++++++++++-------------
 src/engine/script_p.h          |  3 ---
 test/script/script_test.cpp    | 16 ++++++++++++----
 5 files changed, 25 insertions(+), 23 deletions(-)

diff --git a/include/scratchcpp/script.h b/include/scratchcpp/script.h
index 010906f5..fe3a44ee 100644
--- a/include/scratchcpp/script.h
+++ b/include/scratchcpp/script.h
@@ -38,7 +38,6 @@ class LIBSCRATCHCPP_EXPORT Script
         bool runHatPredicate();
 
         void setProcedures(const std::vector<unsigned int *> &procedures);
-        void setFunctions(const std::vector<BlockFunc> &functions);
         void setConstValues(const std::vector<Value> &values);
         void setVariables(const std::vector<Variable *> &variables);
         void setLists(const std::vector<List *> &lists);
@@ -47,6 +46,8 @@ class LIBSCRATCHCPP_EXPORT Script
         std::shared_ptr<VirtualMachine> start(Target *target);
 
     private:
+        BlockFunc *getFunctions() const;
+
         spimpl::unique_impl_ptr<ScriptPrivate> impl;
 };
 
diff --git a/src/engine/internal/engine.cpp b/src/engine/internal/engine.cpp
index 85a21727..8edf260f 100644
--- a/src/engine/internal/engine.cpp
+++ b/src/engine/internal/engine.cpp
@@ -244,7 +244,6 @@ void Engine::compile()
 
         for (auto block : blocks) {
             if (m_scripts.count(block) == 1) {
-                m_scripts[block]->setFunctions(m_functions);
                 m_scripts[block]->setProcedures(procedureBytecodes);
                 m_scripts[block]->setConstValues(compiler.constValues());
                 m_scripts[block]->setVariables(compiler.variables());
@@ -293,7 +292,6 @@ void Engine::compile()
             compiler.end();
 
             script->setBytecode(compiler.bytecode());
-            script->setFunctions(m_functions);
             script->setConstValues(compiler.constValues());
             script->setVariables(compiler.variables());
             script->setLists(compiler.lists());
diff --git a/src/engine/script.cpp b/src/engine/script.cpp
index c770a23a..d544b5b8 100644
--- a/src/engine/script.cpp
+++ b/src/engine/script.cpp
@@ -57,7 +57,6 @@ void Script::setHatPredicateBytecode(const std::vector<unsigned int> &code)
     if (impl->engine && !code.empty()) {
         impl->hatPredicateVm = std::make_shared<VirtualMachine>(impl->engine->stage(), impl->engine, this);
         impl->hatPredicateVm->setBytecode(impl->hatPredicateBytecodeVector.data());
-        impl->hatPredicateVm->setFunctions(impl->functions);
         impl->hatPredicateVm->setConstValues(impl->constValues);
     }
 }
@@ -70,6 +69,7 @@ bool Script::runHatPredicate()
 {
     if (impl->hatPredicateVm && impl->hatPredicateVm->bytecode()) {
         impl->hatPredicateVm->reset();
+        impl->hatPredicateVm->setFunctions(getFunctions());
         impl->hatPredicateVm->run();
         assert(impl->hatPredicateVm->registerCount() == 1);
 
@@ -92,14 +92,14 @@ std::shared_ptr<VirtualMachine> Script::start(Target *target)
     auto vm = std::make_shared<VirtualMachine>(target, impl->engine, this);
     vm->setBytecode(impl->bytecode);
     vm->setProcedures(impl->procedures);
-    vm->setFunctions(impl->functions);
+    vm->setFunctions(getFunctions());
     vm->setConstValues(impl->constValues);
 
     Sprite *sprite = nullptr;
     if (target && !target->isStage())
         sprite = dynamic_cast<Sprite *>(target);
 
-    if (impl->target && sprite && sprite->isClone() && impl->engine) {
+    if (impl->target && sprite && sprite->isClone()) {
         Target *root = sprite->cloneSprite();
 
         if (root != impl->target) {
@@ -156,16 +156,6 @@ void Script::setProcedures(const std::vector<unsigned int *> &procedures)
     impl->procedures = impl->proceduresVector.data();
 }
 
-/*! Sets the list of functions. */
-void Script::setFunctions(const std::vector<BlockFunc> &functions)
-{
-    impl->functionsVector = functions;
-    impl->functions = impl->functionsVector.data();
-
-    if (impl->hatPredicateVm)
-        impl->hatPredicateVm->setFunctions(impl->functions);
-}
-
 /*! Sets the list of constant values. */
 void Script::setConstValues(const std::vector<Value> &values)
 {
@@ -191,3 +181,11 @@ void Script::setLists(const std::vector<List *> &lists)
 {
     impl->lists = lists;
 }
+
+BlockFunc *Script::getFunctions() const
+{
+    if (impl->engine)
+        return const_cast<BlockFunc *>(impl->engine->blockFunctions().data());
+
+    return nullptr;
+}
diff --git a/src/engine/script_p.h b/src/engine/script_p.h
index 3d7ca6a1..d3f991a4 100644
--- a/src/engine/script_p.h
+++ b/src/engine/script_p.h
@@ -33,9 +33,6 @@ struct ScriptPrivate
         unsigned int **procedures = nullptr;
         std::vector<unsigned int *> proceduresVector;
 
-        BlockFunc *functions = nullptr;
-        std::vector<BlockFunc> functionsVector;
-
         const Value *constValues = nullptr;
         std::vector<Value> constValuesVector;
 
diff --git a/test/script/script_test.cpp b/test/script/script_test.cpp
index 4fb5bbae..9f687d53 100644
--- a/test/script/script_test.cpp
+++ b/test/script/script_test.cpp
@@ -12,6 +12,7 @@
 using namespace libscratchcpp;
 
 using ::testing::Return;
+using ::testing::ReturnRef;
 using ::testing::_;
 
 class ScriptTest : public testing::Test
@@ -73,10 +74,13 @@ TEST_F(ScriptTest, HatPredicate)
         return 0;
     };
 
+    std::vector<BlockFunc> functions1 = { f1 };
+    std::vector<BlockFunc> functions2 = { f1, f2, f3 };
+
+    EXPECT_CALL(m_engine, blockFunctions()).WillOnce(ReturnRef(functions1));
     stageTest = nullptr;
     engineTest = nullptr;
     scriptTest = nullptr;
-    script.setFunctions({ f1 });
     script.setConstValues({ "test" });
     script.setHatPredicateBytecode({ vm::OP_START, vm::OP_CONST, 0, vm::OP_PRINT, vm::OP_EXEC, 0, vm::OP_HALT });
     testing::internal::CaptureStdout();
@@ -86,11 +90,11 @@ TEST_F(ScriptTest, HatPredicate)
     ASSERT_EQ(engineTest, &m_engine);
     ASSERT_EQ(scriptTest, &script);
 
+    EXPECT_CALL(m_engine, blockFunctions()).WillOnce(ReturnRef(functions2));
     stageTest = nullptr;
     engineTest = nullptr;
     scriptTest = nullptr;
     script.setHatPredicateBytecode({ vm::OP_START, vm::OP_CONST, 0, vm::OP_PRINT, vm::OP_EXEC, 1, vm::OP_HALT });
-    script.setFunctions({ f1, f2, f3 });
     script.setConstValues({ 5 });
     testing::internal::CaptureStdout();
     ASSERT_TRUE(script.runHatPredicate());
@@ -99,6 +103,7 @@ TEST_F(ScriptTest, HatPredicate)
     ASSERT_EQ(engineTest, &m_engine);
     ASSERT_EQ(scriptTest, &script);
 
+    EXPECT_CALL(m_engine, blockFunctions()).WillOnce(ReturnRef(functions2));
     stageTest = nullptr;
     engineTest = nullptr;
     scriptTest = nullptr;
@@ -119,6 +124,7 @@ TEST_F(ScriptTest, Start)
     static std::vector<unsigned int> bytecode = { vm::OP_START, vm::OP_HALT };
     static std::vector<unsigned int *> procedures = { bytecode.data() };
     static std::vector<BlockFunc> functions = { &testFunction };
+    static std::vector<BlockFunc> noFunctions;
     static std::vector<Value> constValues = { "test" };
 
     std::shared_ptr<Variable> var1 = std::make_unique<Variable>("a", "", Value());
@@ -144,6 +150,7 @@ TEST_F(ScriptTest, Start)
 
     Script script2(&m_target, nullptr, &m_engine);
 
+    EXPECT_CALL(m_engine, blockFunctions()).WillOnce(ReturnRef(noFunctions));
     vm = script2.start();
     ASSERT_TRUE(vm);
     ASSERT_EQ(vm->target(), &m_target);
@@ -152,11 +159,11 @@ TEST_F(ScriptTest, Start)
     Script script3(&m_target, nullptr, &m_engine);
     script3.setBytecode(bytecode);
     script3.setProcedures(procedures);
-    script3.setFunctions(functions);
     script3.setConstValues(constValues);
     script3.setVariables(variables);
     script3.setLists(lists);
 
+    EXPECT_CALL(m_engine, blockFunctions()).WillOnce(ReturnRef(functions));
     vm = script3.start();
     ASSERT_TRUE(vm);
     ASSERT_EQ(vm->bytecode()[0], bytecode[0]);
@@ -166,6 +173,7 @@ TEST_F(ScriptTest, Start)
     ASSERT_EQ(vm->variables()[0], variables[0]->valuePtr());
     ASSERT_EQ(vm->lists()[0], lists[0]);
 
+    EXPECT_CALL(m_engine, blockFunctions()).WillOnce(ReturnRef(functions));
     Target target;
     target.addVariable(var1);
     target.addList(list1);
@@ -194,11 +202,11 @@ TEST_F(ScriptTest, Start)
     Script script4(&root, nullptr, &m_engine);
     script4.setBytecode(bytecode);
     script4.setProcedures(procedures);
-    script4.setFunctions(functions);
     script4.setConstValues(constValues);
     script4.setVariables(variables);
     script4.setLists(lists);
 
+    EXPECT_CALL(m_engine, blockFunctions()).WillOnce(ReturnRef(functions));
     vm = script4.start(clone.get());
 
     ASSERT_TRUE(vm);

From 2136523c0825a4c4d45a740ab15ab70c48fa9184 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Mon, 18 Mar 2024 09:53:35 +0100
Subject: [PATCH 38/77] fix #462: Implement hex, octal and bin conversion

---
 include/scratchcpp/value.h          |  71 +++++++++
 test/scratch_classes/value_test.cpp | 225 ++++++++++++++++++++++++++++
 2 files changed, 296 insertions(+)

diff --git a/include/scratchcpp/value.h b/include/scratchcpp/value.h
index 32180df0..7f872213 100644
--- a/include/scratchcpp/value.h
+++ b/include/scratchcpp/value.h
@@ -766,6 +766,51 @@ class LIBSCRATCHCPP_EXPORT Value
             return (s1.compare(s2) == 0);
         }
 
+        static long hexToDec(const std::string &s)
+        {
+            static const std::string digits = "0123456789abcdef";
+
+            for (char c : s) {
+                if (digits.find(c) == std::string::npos) {
+                    return 0;
+                }
+            }
+
+            std::istringstream stream(s);
+            long ret;
+            stream >> std::hex >> ret;
+            return ret;
+        }
+
+        static long octToDec(const std::string &s)
+        {
+            static const std::string digits = "01234567";
+
+            for (char c : s) {
+                if (digits.find(c) == std::string::npos) {
+                    return 0;
+                }
+            }
+
+            std::istringstream stream(s);
+            long ret;
+            stream >> std::oct >> ret;
+            return ret;
+        }
+
+        static double binToDec(const std::string &s)
+        {
+            static const std::string digits = "01";
+
+            for (char c : s) {
+                if (digits.find(c) == std::string::npos) {
+                    return 0;
+                }
+            }
+
+            return std::stoi(s, 0, 2);
+        }
+
         static double stringToDouble(const std::string &s, bool *ok = nullptr)
         {
             if (ok)
@@ -785,6 +830,19 @@ class LIBSCRATCHCPP_EXPORT Value
                 return 0;
             }
 
+            if (s.size() >= 2 && s[0] == '0') {
+                std::string sub = s.substr(2, s.size() - 2);
+                std::transform(sub.begin(), sub.end(), sub.begin(), ::tolower);
+
+                if (s[1] == 'x' || s[1] == 'X') {
+                    return hexToDec(sub);
+                } else if (s[1] == 'o' || s[1] == 'O') {
+                    return octToDec(sub);
+                } else if (s[1] == 'b' || s[1] == 'B') {
+                    return binToDec(sub);
+                }
+            }
+
             static const std::string digits = "0123456789.eE+-";
             const std::string *stringPtr = &s;
             bool customStr = false;
@@ -850,6 +908,19 @@ class LIBSCRATCHCPP_EXPORT Value
                 return 0;
             }
 
+            if (s.size() >= 2 && s[0] == '0') {
+                std::string sub = s.substr(2, s.size() - 2);
+                std::transform(sub.begin(), sub.end(), sub.begin(), ::tolower);
+
+                if (s[1] == 'x' || s[1] == 'X') {
+                    return hexToDec(sub);
+                } else if (s[1] == 'o' || s[1] == 'O') {
+                    return octToDec(sub);
+                } else if (s[1] == 'b' || s[1] == 'B') {
+                    return binToDec(sub);
+                }
+            }
+
             static const std::string digits = "0123456789+-";
 
             for (char c : s) {
diff --git a/test/scratch_classes/value_test.cpp b/test/scratch_classes/value_test.cpp
index c46be444..de214c86 100644
--- a/test/scratch_classes/value_test.cpp
+++ b/test/scratch_classes/value_test.cpp
@@ -760,6 +760,61 @@ TEST(ValueTest, ToInt)
 
     v = "something";
     ASSERT_EQ(v.toInt(), 0);
+
+    // Hex
+    v = "0xafe";
+    ASSERT_TRUE(v.isString());
+    ASSERT_EQ(v.toInt(), 2814);
+
+    v = "0x0afe";
+    ASSERT_TRUE(v.isString());
+    ASSERT_EQ(v.toInt(), 2814);
+
+    v = "0xBaCD";
+    ASSERT_TRUE(v.isString());
+    ASSERT_EQ(v.toInt(), 47821);
+
+    v = "0XBaCD";
+    ASSERT_TRUE(v.isString());
+    ASSERT_EQ(v.toInt(), 47821);
+
+    v = "0xAbG";
+    ASSERT_TRUE(v.isString());
+    ASSERT_EQ(v.toInt(), 0);
+
+    // Octal
+    v = "0o506";
+    ASSERT_TRUE(v.isString());
+    ASSERT_EQ(v.toInt(), 326);
+
+    v = "0o0506";
+    ASSERT_TRUE(v.isString());
+    ASSERT_EQ(v.toInt(), 326);
+
+    v = "0O17206";
+    ASSERT_TRUE(v.isString());
+    ASSERT_EQ(v.toInt(), 7814);
+
+    v = "0o5783";
+    ASSERT_TRUE(v.isString());
+    ASSERT_EQ(v.toInt(), 0);
+
+    // Binary
+    v = "0b101101";
+    ASSERT_TRUE(v.isString());
+    ASSERT_EQ(v.toInt(), 45);
+
+    v = "0b0101101";
+    ASSERT_TRUE(v.isString());
+    ASSERT_EQ(v.toInt(), 45);
+
+    v = "0B1110100110";
+    ASSERT_TRUE(v.isString());
+    ASSERT_EQ(v.toInt(), 934);
+
+    v = "0b100112001";
+    ASSERT_TRUE(v.isString());
+    ASSERT_EQ(v.toInt(), 0);
 }
 
 TEST(ValueTest, ToLong)
@@ -808,6 +863,61 @@ TEST(ValueTest, ToLong)
 
     v = "something";
     ASSERT_EQ(v.toLong(), 0);
+
+    // Hex
+    v = "0xafe";
+    ASSERT_TRUE(v.isString());
+    ASSERT_EQ(v.toLong(), 2814);
+
+    v = "0x0afe";
+    ASSERT_TRUE(v.isString());
+    ASSERT_EQ(v.toLong(), 2814);
+
+    v = "0xBaCD";
+    ASSERT_TRUE(v.isString());
+    ASSERT_EQ(v.toLong(), 47821);
+
+    v = "0XBaCD";
+    ASSERT_TRUE(v.isString());
+    ASSERT_EQ(v.toLong(), 47821);
+
+    v = "0xAbG";
+    ASSERT_TRUE(v.isString());
+    ASSERT_EQ(v.toLong(), 0);
+
+    // Octal
+    v = "0o506";
+    ASSERT_TRUE(v.isString());
+    ASSERT_EQ(v.toLong(), 326);
+
+    v = "0o0506";
+    ASSERT_TRUE(v.isString());
+    ASSERT_EQ(v.toLong(), 326);
+
+    v = "0O17206";
+    ASSERT_TRUE(v.isString());
+    ASSERT_EQ(v.toLong(), 7814);
+
+    v = "0o5783";
+    ASSERT_TRUE(v.isString());
+    ASSERT_EQ(v.toLong(), 0);
+
+    // Binary
+    v = "0b101101";
+    ASSERT_TRUE(v.isString());
+    ASSERT_EQ(v.toLong(), 45);
+
+    v = "0b0101101";
+    ASSERT_TRUE(v.isString());
+    ASSERT_EQ(v.toLong(), 45);
+
+    v = "0B1110100110";
+    ASSERT_TRUE(v.isString());
+    ASSERT_EQ(v.toLong(), 934);
+
+    v = "0b100112001";
+    ASSERT_TRUE(v.isString());
+    ASSERT_EQ(v.toLong(), 0);
 }
 
 TEST(ValueTest, ToDouble)
@@ -866,6 +976,61 @@ TEST(ValueTest, ToDouble)
 
     v = "something";
     ASSERT_EQ(v.toDouble(), 0.0);
+
+    // Hex
+    v = "0xafe";
+    ASSERT_TRUE(v.isString());
+    ASSERT_EQ(v.toDouble(), 2814);
+
+    v = "0x0afe";
+    ASSERT_TRUE(v.isString());
+    ASSERT_EQ(v.toDouble(), 2814);
+
+    v = "0xBaCD";
+    ASSERT_TRUE(v.isString());
+    ASSERT_EQ(v.toDouble(), 47821);
+
+    v = "0XBaCD";
+    ASSERT_TRUE(v.isString());
+    ASSERT_EQ(v.toDouble(), 47821);
+
+    v = "0xAbG";
+    ASSERT_TRUE(v.isString());
+    ASSERT_EQ(v.toDouble(), 0);
+
+    // Octal
+    v = "0o506";
+    ASSERT_TRUE(v.isString());
+    ASSERT_EQ(v.toDouble(), 326);
+
+    v = "0o0506";
+    ASSERT_TRUE(v.isString());
+    ASSERT_EQ(v.toDouble(), 326);
+
+    v = "0O17206";
+    ASSERT_TRUE(v.isString());
+    ASSERT_EQ(v.toDouble(), 7814);
+
+    v = "0o5783";
+    ASSERT_TRUE(v.isString());
+    ASSERT_EQ(v.toDouble(), 0);
+
+    // Binary
+    v = "0b101101";
+    ASSERT_TRUE(v.isString());
+    ASSERT_EQ(v.toDouble(), 45);
+
+    v = "0b0101101";
+    ASSERT_TRUE(v.isString());
+    ASSERT_EQ(v.toDouble(), 45);
+
+    v = "0B1110100110";
+    ASSERT_TRUE(v.isString());
+    ASSERT_EQ(v.toDouble(), 934);
+
+    v = "0b100112001";
+    ASSERT_TRUE(v.isString());
+    ASSERT_EQ(v.toDouble(), 0);
 }
 
 TEST(ValueTest, ToBool)
@@ -921,6 +1086,66 @@ TEST(ValueTest, ToBool)
 
     v = "something";
     ASSERT_EQ(v.toBool(), false);
+
+    // TODO: Uncomment this (#517)
+    // Hex
+    /*v = "0xafe";
+    ASSERT_TRUE(v.isString());
+    ASSERT_TRUE(v.toBool());
+
+    v = "0x0afe";
+    ASSERT_TRUE(v.isString());
+    ASSERT_TRUE(v.toBool());
+
+    v = "0xBaCD";
+    ASSERT_TRUE(v.isString());
+    ASSERT_TRUE(v.toBool());
+
+    v = "0x0";
+    ASSERT_TRUE(v.isString());
+    ASSERT_FALSE(v.toBool());
+
+    v = "0XBaCD";
+    ASSERT_TRUE(v.isString());
+    ASSERT_TRUE(v.toBool());
+
+    v = "0xAbG";
+    ASSERT_TRUE(v.isString());
+    ASSERT_FALSE(v.toBool());
+
+    // Octal
+    v = "0o506";
+    ASSERT_TRUE(v.isString());
+    ASSERT_TRUE(v.toBool());
+
+    v = "0o0506";
+    ASSERT_TRUE(v.isString());
+    ASSERT_TRUE(v.toBool());
+
+    v = "0O17206";
+    ASSERT_TRUE(v.isString());
+    ASSERT_TRUE(v.toBool());
+
+    v = "0o5783";
+    ASSERT_TRUE(v.isString());
+    ASSERT_FALSE(v.toBool());
+
+    // Binary
+    v = "0b101101";
+    ASSERT_TRUE(v.isString());
+    ASSERT_TRUE(v.toBool());
+
+    v = "0b0101101";
+    ASSERT_TRUE(v.isString());
+    ASSERT_TRUE(v.toBool());
+
+    v = "0B1110100110";
+    ASSERT_TRUE(v.isString());
+    ASSERT_TRUE(v.toBool());
+
+    v = "0b100112001";
+    ASSERT_TRUE(v.isString());
+    ASSERT_FALSE(v.toBool());*/
 }
 
 TEST(ValueTest, ToString)

From 597c4a0d29f42d37117aefc282761a60938ad97d Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Wed, 20 Mar 2024 16:17:02 +0100
Subject: [PATCH 39/77] Store values of edge-activated hats with multiple field
 values correctly

---
 src/engine/internal/engine.cpp |  18 +++++++++--
 src/engine/internal/engine.h   |   2 +-
 test/engine/engine_test.cpp    |  55 +++++++++++++++++++++++++++++----
 test/when_greater_than.sb3     | Bin 943 -> 1124 bytes
 4 files changed, 65 insertions(+), 10 deletions(-)

diff --git a/src/engine/internal/engine.cpp b/src/engine/internal/engine.cpp
index 8edf260f..1ad56431 100644
--- a/src/engine/internal/engine.cpp
+++ b/src/engine/internal/engine.cpp
@@ -453,14 +453,26 @@ void Engine::step()
             // Processing the hats means running their predicates (if they didn't change their return value from false to true, remove the threads)
             for (auto thread : newThreads) {
                 bool oldValue = false;
+                auto hatBlock = thread->script()->topBlock();
+                assert(hatBlock);
+                assert(hatBlock->fieldAt(0)); // TODO: Edge-activated hats currently support only one field
+                int fieldValueId = hatBlock->fieldAt(0)->specialValueId();
+                assert(fieldValueId != -1);
                 auto it = m_edgeActivatedHatValues.find(hatType);
 
-                if (it != m_edgeActivatedHatValues.cend())
-                    oldValue = it->second;
+                if (it == m_edgeActivatedHatValues.cend()) {
+                    m_edgeActivatedHatValues[hatType] = {};
+                } else {
+                    const std::unordered_map<int, bool> &values = it->second;
+                    auto fieldIt = values.find(fieldValueId);
+
+                    if (fieldIt != values.cend())
+                        oldValue = fieldIt->second;
+                }
 
                 bool newValue = thread->script()->runHatPredicate();
                 bool edgeWasActivated = !oldValue && newValue; // changed from false true
-                m_edgeActivatedHatValues[hatType] = newValue;
+                m_edgeActivatedHatValues[hatType][fieldValueId] = newValue;
 
                 if (!edgeWasActivated)
                     stopThread(thread.get());
diff --git a/src/engine/internal/engine.h b/src/engine/internal/engine.h
index 44ac44fb..d73cc196 100644
--- a/src/engine/internal/engine.h
+++ b/src/engine/internal/engine.h
@@ -248,7 +248,7 @@ class Engine : public IEngine
 
         std::unordered_map<Script *, std::unordered_map<HatField, int>> m_scriptHatFields; // HatField, field ID from the block implementation
 
-        std::unordered_map<HatType, bool> m_edgeActivatedHatValues; // edge-activated hats only run after the value changes from false to true
+        std::unordered_map<HatType, std::unordered_map<int, bool>> m_edgeActivatedHatValues; // (field value, last value) edge-activated hats only run after the value changes from false to true
 
         std::unique_ptr<ITimer> m_defaultTimer;
         ITimer *m_timer = nullptr;
diff --git a/test/engine/engine_test.cpp b/test/engine/engine_test.cpp
index 1929c7a3..1e44ec6f 100644
--- a/test/engine/engine_test.cpp
+++ b/test/engine/engine_test.cpp
@@ -14,8 +14,10 @@
 #include <scratch/sound_p.h>
 #include <timermock.h>
 #include <clockmock.h>
+#include <audioinputmock.h>
 #include <audiooutputmock.h>
 #include <audioplayermock.h>
+#include <audioloudnessmock.h>
 #include <monitorhandlermock.h>
 #include <blocksectionmock.h>
 #include <thread>
@@ -27,6 +29,7 @@
 // TODO: Remove this
 #include "blocks/variableblocks.h"
 #include "blocks/listblocks.h"
+#include "blocks/eventblocks.h"
 
 using namespace libscratchcpp;
 
@@ -1936,24 +1939,64 @@ TEST(EngineTest, EdgeActivatedHats)
     Stage *stage = engine->stage();
     ASSERT_TRUE(stage);
 
-    ASSERT_VAR(stage, "test");
-    auto var = GET_VAR(stage, "test");
-    ASSERT_EQ(var->value().toInt(), 0);
+    ASSERT_VAR(stage, "test1");
+    auto var1 = GET_VAR(stage, "test1");
+    ASSERT_VAR(stage, "test2");
+    auto var2 = GET_VAR(stage, "test2");
+
+    ASSERT_EQ(var1->value().toInt(), 0);
+    ASSERT_EQ(var2->value().toInt(), 0);
 
     TimerMock timer;
+    AudioInputMock audioInput;
     engine->setTimer(&timer);
+    EventBlocks::audioInput = &audioInput;
+    auto audioLoudness = std::make_shared<AudioLoudnessMock>();
+    EXPECT_CALL(audioInput, audioLoudness()).WillRepeatedly(Return(audioLoudness));
 
     EXPECT_CALL(timer, value()).WillOnce(Return(5));
+    EXPECT_CALL(*audioLoudness, getLoudness()).WillOnce(Return(1));
     engine->step();
-    ASSERT_EQ(var->value().toInt(), 0);
+    ASSERT_EQ(var1->value().toInt(), 0);
+    ASSERT_EQ(var2->value().toInt(), 0);
 
     EXPECT_CALL(timer, value()).WillOnce(Return(10));
+    EXPECT_CALL(*audioLoudness, getLoudness()).WillOnce(Return(9));
     engine->step();
-    ASSERT_EQ(var->value().toInt(), 0);
+    ASSERT_EQ(var1->value().toInt(), 0);
+    ASSERT_EQ(var2->value().toInt(), 1);
 
     EXPECT_CALL(timer, value()).WillOnce(Return(10.2));
+    EXPECT_CALL(*audioLoudness, getLoudness()).WillOnce(Return(10));
+    engine->step();
+    ASSERT_EQ(var1->value().toInt(), 1);
+    ASSERT_EQ(var2->value().toInt(), 1);
+
+    EXPECT_CALL(timer, value()).WillOnce(Return(15));
+    EXPECT_CALL(*audioLoudness, getLoudness()).WillOnce(Return(2));
     engine->step();
-    ASSERT_EQ(var->value().toInt(), 1);
+    ASSERT_EQ(var1->value().toInt(), 1);
+    ASSERT_EQ(var2->value().toInt(), 1);
+
+    EXPECT_CALL(timer, value()).WillOnce(Return(12));
+    EXPECT_CALL(*audioLoudness, getLoudness()).WillOnce(Return(8));
+    engine->step();
+    ASSERT_EQ(var1->value().toInt(), 1);
+    ASSERT_EQ(var2->value().toInt(), 2);
+
+    EXPECT_CALL(timer, value()).WillOnce(Return(8));
+    EXPECT_CALL(*audioLoudness, getLoudness()).WillOnce(Return(3));
+    engine->step();
+    ASSERT_EQ(var1->value().toInt(), 1);
+    ASSERT_EQ(var2->value().toInt(), 2);
+
+    EXPECT_CALL(timer, value()).WillOnce(Return(11));
+    EXPECT_CALL(*audioLoudness, getLoudness()).WillOnce(Return(15));
+    engine->step();
+    ASSERT_EQ(var1->value().toInt(), 2);
+    ASSERT_EQ(var2->value().toInt(), 3);
+
+    EventBlocks::audioInput = nullptr;
 }
 
 TEST(EngineTest, UserAgent)
diff --git a/test/when_greater_than.sb3 b/test/when_greater_than.sb3
index edb9e1dab5b7fe78ba842d8686bd757e460ff49d..7155862ff910eee891f0f2f74387b3bf407b5201 100644
GIT binary patch

From c396baf32490f294934321b46451209c285f3d21 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Thu, 21 Mar 2024 15:31:59 +0100
Subject: [PATCH 40/77] Add sigslot library

---
 include/scratchcpp/signal.h | 1723 +++++++++++++++++++++++++++++++++++
 1 file changed, 1723 insertions(+)
 create mode 100644 include/scratchcpp/signal.h

diff --git a/include/scratchcpp/signal.h b/include/scratchcpp/signal.h
new file mode 100644
index 00000000..08f0fa6f
--- /dev/null
+++ b/include/scratchcpp/signal.h
@@ -0,0 +1,1723 @@
+#pragma once
+#include <atomic>
+#include <cstring>
+#include <memory>
+#include <mutex>
+#include <type_traits>
+#include <utility>
+#include <thread>
+#include <vector>
+
+#if defined(__GXX_RTTI) || defined(__cpp_rtti) || defined(_CPPRTTI)
+#define SIGSLOT_RTTI_ENABLED 1
+#include <typeinfo>
+#endif
+
+namespace libscratchcpp::sigslot
+{
+
+template<typename, typename...>
+class signal_base;
+
+namespace detail
+{
+
+// Used to detect an object of observer type
+struct observer_type
+{ };
+
+} // namespace detail
+
+namespace trait
+{
+
+/// represent a list of types
+template<typename...>
+struct typelist
+{
+};
+
+/**
+ * Pointers that can be converted to a weak pointer concept for tracking
+ * purpose must implement the to_weak() function in order to make use of
+ * ADL to convert that type and make it usable
+ */
+
+template<typename T>
+std::weak_ptr<T> to_weak(std::weak_ptr<T> w)
+{
+    return w;
+}
+
+template<typename T>
+std::weak_ptr<T> to_weak(std::shared_ptr<T> s)
+{
+    return s;
+}
+
+// tools
+namespace detail
+{
+
+template<typename...>
+struct voider
+{
+        using type = void;
+};
+
+// void_t from c++17
+template<typename... T>
+using void_t = typename detail::voider<T...>::type;
+
+template<typename, typename = void>
+struct has_call_operator : std::false_type
+{
+};
+
+template<typename F>
+struct has_call_operator<F, void_t<decltype(&std::remove_reference<F>::type::operator())>> : std::true_type
+{
+};
+
+template<typename, typename, typename = void, typename = void>
+struct is_callable : std::false_type
+{
+};
+
+template<typename F, typename P, typename... T>
+struct is_callable<F, P, typelist<T...>, void_t<decltype(((*std::declval<P>()).*std::declval<F>())(std::declval<T>()...))>> : std::true_type
+{
+};
+
+template<typename F, typename... T>
+struct is_callable<F, typelist<T...>, void_t<decltype(std::declval<F>()(std::declval<T>()...))>> : std::true_type
+{
+};
+
+template<typename T, typename = void>
+struct is_weak_ptr : std::false_type
+{
+};
+
+template<typename T>
+struct is_weak_ptr<T, void_t<decltype(std::declval<T>().expired()), decltype(std::declval<T>().lock()), decltype(std::declval<T>().reset())>> : std::true_type
+{
+};
+
+template<typename T, typename = void>
+struct is_weak_ptr_compatible : std::false_type
+{
+};
+
+template<typename T>
+struct is_weak_ptr_compatible<T, void_t<decltype(to_weak(std::declval<T>()))>> : is_weak_ptr<decltype(to_weak(std::declval<T>()))>
+{
+};
+
+template<typename...>
+struct is_signal : std::false_type
+{
+};
+
+template<typename L, typename... T>
+struct is_signal<signal_base<L, T...>> : std::true_type
+{
+};
+
+} // namespace detail
+
+static constexpr bool with_rtti =
+#ifdef SIGSLOT_RTTI_ENABLED
+    true;
+#else
+    false;
+#endif
+
+/// determine if a pointer is convertible into a "weak" pointer
+template<typename P>
+constexpr bool is_weak_ptr_compatible_v = detail::is_weak_ptr_compatible<std::decay_t<P>>::value;
+
+/// determine if a type T (Callable or Pmf) is callable with supplied arguments
+template<typename L, typename... T>
+constexpr bool is_callable_v = detail::is_callable<T..., L>::value;
+
+template<typename T>
+constexpr bool is_weak_ptr_v = detail::is_weak_ptr<T>::value;
+
+template<typename T>
+constexpr bool has_call_operator_v = detail::has_call_operator<T>::value;
+
+template<typename T>
+constexpr bool is_pointer_v = std::is_pointer<T>::value;
+
+template<typename T>
+constexpr bool is_func_v = std::is_function<T>::value;
+
+template<typename T>
+constexpr bool is_pmf_v = std::is_member_function_pointer<T>::value;
+
+template<typename T>
+constexpr bool is_observer_v = std::is_base_of<sigslot::detail::observer_type, std::remove_pointer_t<std::remove_reference_t<T>>>::value;
+
+template<typename S>
+constexpr bool is_signal_v = detail::is_signal<S>::value;
+
+} // namespace trait
+
+/**
+ * A group_id is used to identify a group of slots
+ */
+using group_id = std::int32_t;
+
+namespace detail
+{
+
+/**
+ * The following function_traits and object_pointer series of templates are
+ * used to circumvent the type-erasing that takes place in the slot_base
+ * implementations. They are used to compare the stored functions and objects
+ * with another one for disconnection purpose.
+ */
+
+/*
+ * Function pointers and member function pointers size differ from compiler to
+ * compiler, and for virtual members compared to non virtual members. On some
+ * compilers, multiple inheritance has an impact too. Hence, we form an union
+ * big enough to store any kind of function pointer.
+ */
+namespace mock
+{
+
+struct a
+{
+        virtual ~a() = default;
+        void f();
+        virtual void g();
+        static void h();
+};
+struct b
+{
+        virtual ~b() = default;
+        void f();
+        virtual void g();
+};
+struct c
+    : a
+    , b
+{
+        void f();
+        void g() override;
+};
+struct d : virtual a
+{
+        void g() override;
+};
+
+union fun_types
+{
+        decltype(&d::g) dm;
+        decltype(&c::g) mm;
+        decltype(&c::g) mvm;
+        decltype(&a::f) m;
+        decltype(&a::g) vm;
+        decltype(&a::h) s;
+        void (*f)();
+        void *o;
+};
+
+} // namespace mock
+
+/*
+ * This struct is used to store function pointers.
+ * This is needed for slot disconnection by function pointer.
+ * It assumes the underlying implementation to be trivially copiable.
+ */
+struct func_ptr
+{
+        func_ptr() :
+            sz{ 0 }
+        {
+            std::uninitialized_fill(std::begin(data), std::end(data), '\0');
+        }
+
+        template<typename T>
+        void store(const T &t)
+        {
+            const auto *b = reinterpret_cast<const char *>(&t);
+            sz = sizeof(T);
+            std::memcpy(data, b, sz);
+        }
+
+        template<typename T>
+        const T *as() const
+        {
+            if (sizeof(T) != sz) {
+                return nullptr;
+            }
+            return reinterpret_cast<const T *>(data);
+        }
+
+    private:
+        alignas(sizeof(mock::fun_types)) char data[sizeof(mock::fun_types)];
+        size_t sz;
+};
+
+template<typename T, typename = void>
+struct function_traits
+{
+        static void ptr(const T & /*t*/, func_ptr & /*d*/) { }
+
+        static bool eq(const T & /*t*/, const func_ptr & /*d*/) { return false; }
+
+        static constexpr bool is_disconnectable = false;
+        static constexpr bool must_check_object = true;
+};
+
+template<typename T>
+struct function_traits<T, std::enable_if_t<trait::is_func_v<T>>>
+{
+        static void ptr(T &t, func_ptr &d) { d.store(&t); }
+
+        static bool eq(T &t, const func_ptr &d)
+        {
+            const auto *r = d.as<const T *>();
+            return r && *r == &t;
+        }
+
+        static constexpr bool is_disconnectable = true;
+        static constexpr bool must_check_object = false;
+};
+
+template<typename T>
+struct function_traits<T *, std::enable_if_t<trait::is_func_v<T>>>
+{
+        static void ptr(T *t, func_ptr &d) { function_traits<T>::ptr(*t, d); }
+
+        static bool eq(T *t, const func_ptr &d) { return function_traits<T>::eq(*t, d); }
+
+        static constexpr bool is_disconnectable = true;
+        static constexpr bool must_check_object = false;
+};
+
+template<typename T>
+struct function_traits<T, std::enable_if_t<trait::is_pmf_v<T>>>
+{
+        static void ptr(T t, func_ptr &d) { d.store(t); }
+
+        static bool eq(T t, const func_ptr &d)
+        {
+            const auto *r = d.as<const T>();
+            return r && *r == t;
+        }
+
+        static constexpr bool is_disconnectable = trait::with_rtti;
+        static constexpr bool must_check_object = true;
+};
+
+// for function objects, the assumption is that we are looking for the call operator
+template<typename T>
+struct function_traits<T, std::enable_if_t<trait::has_call_operator_v<T>>>
+{
+        using call_type = decltype(&std::remove_reference<T>::type::operator());
+
+        static void ptr(const T & /*t*/, func_ptr &d) { function_traits<call_type>::ptr(&T::operator(), d); }
+
+        static bool eq(const T & /*t*/, const func_ptr &d) { return function_traits<call_type>::eq(&T::operator(), d); }
+
+        static constexpr bool is_disconnectable = function_traits<call_type>::is_disconnectable;
+        static constexpr bool must_check_object = function_traits<call_type>::must_check_object;
+};
+
+template<typename T>
+func_ptr get_function_ptr(const T &t)
+{
+    func_ptr d;
+    function_traits<std::decay_t<T>>::ptr(t, d);
+    return d;
+}
+
+template<typename T>
+bool eq_function_ptr(const T &t, const func_ptr &d)
+{
+    return function_traits<std::decay_t<T>>::eq(t, d);
+}
+
+/*
+ * obj_ptr is used to store a pointer to an object.
+ * The object_pointer traits are needed to handle trackable objects correctly,
+ * as they are likely to not be pointers.
+ */
+using obj_ptr = const void *;
+
+template<typename T>
+obj_ptr get_object_ptr(const T &t);
+
+template<typename T, typename = void>
+struct object_pointer
+{
+        static obj_ptr get(const T &) { return nullptr; }
+};
+
+template<typename T>
+struct object_pointer<T *, std::enable_if_t<trait::is_pointer_v<T *>>>
+{
+        static obj_ptr get(const T *t) { return reinterpret_cast<obj_ptr>(t); }
+};
+
+template<typename T>
+struct object_pointer<T, std::enable_if_t<trait::is_weak_ptr_v<T>>>
+{
+        static obj_ptr get(const T &t)
+        {
+            auto p = t.lock();
+            return get_object_ptr(p);
+        }
+};
+
+template<typename T>
+struct object_pointer<T, std::enable_if_t<!trait::is_pointer_v<T> && !trait::is_weak_ptr_v<T> && trait::is_weak_ptr_compatible_v<T>>>
+{
+        static obj_ptr get(const T &t) { return t ? reinterpret_cast<obj_ptr>(t.get()) : nullptr; }
+};
+
+template<typename T>
+obj_ptr get_object_ptr(const T &t)
+{
+    return object_pointer<T>::get(t);
+}
+
+// noop mutex for thread-unsafe use
+struct null_mutex
+{
+        null_mutex() noexcept = default;
+        ~null_mutex() noexcept = default;
+        null_mutex(const null_mutex &) = delete;
+        null_mutex &operator=(const null_mutex &) = delete;
+        null_mutex(null_mutex &&) = delete;
+        null_mutex &operator=(null_mutex &&) = delete;
+
+        inline bool try_lock() noexcept { return true; }
+        inline void lock() noexcept { }
+        inline void unlock() noexcept { }
+};
+
+/**
+ * A spin mutex that yields, mostly for use in benchmarks and scenarii that invoke
+ * slots at a very high pace.
+ * One should almost always prefer a standard mutex over this.
+ */
+struct spin_mutex
+{
+        spin_mutex() noexcept = default;
+        ~spin_mutex() noexcept = default;
+        spin_mutex(spin_mutex const &) = delete;
+        spin_mutex &operator=(const spin_mutex &) = delete;
+        spin_mutex(spin_mutex &&) = delete;
+        spin_mutex &operator=(spin_mutex &&) = delete;
+
+        void lock() noexcept
+        {
+            while (true) {
+                while (!state.load(std::memory_order_relaxed)) {
+                    std::this_thread::yield();
+                }
+
+                if (try_lock()) {
+                    break;
+                }
+            }
+        }
+
+        bool try_lock() noexcept { return state.exchange(false, std::memory_order_acquire); }
+
+        void unlock() noexcept { state.store(true, std::memory_order_release); }
+
+    private:
+        std::atomic<bool> state{ true };
+};
+
+/**
+ * A simple copy on write container that will be used to improve slot lists
+ * access efficiency in a multithreaded context.
+ */
+template<typename T>
+class copy_on_write
+{
+        struct payload
+        {
+                payload() = default;
+
+                template<typename... Args>
+                explicit payload(Args &&...args) :
+                    value(std::forward<Args>(args)...)
+                {
+                }
+
+                std::atomic<std::size_t> count{ 1 };
+                T value;
+        };
+
+    public:
+        using element_type = T;
+
+        copy_on_write() :
+            m_data(new payload)
+        {
+        }
+
+        template<typename U>
+        explicit copy_on_write(U &&x, std::enable_if_t<!std::is_same<std::decay_t<U>, copy_on_write>::value> * = nullptr) :
+            m_data(new payload(std::forward<U>(x)))
+        {
+        }
+
+        copy_on_write(const copy_on_write &x) noexcept :
+            m_data(x.m_data)
+        {
+            ++m_data->count;
+        }
+
+        copy_on_write(copy_on_write &&x) noexcept :
+            m_data(x.m_data)
+        {
+            x.m_data = nullptr;
+        }
+
+        ~copy_on_write()
+        {
+            if (m_data && (--m_data->count == 0)) {
+                delete m_data;
+            }
+        }
+
+        copy_on_write &operator=(const copy_on_write &x) noexcept
+        {
+            if (&x != this) {
+                *this = copy_on_write(x);
+            }
+            return *this;
+        }
+
+        copy_on_write &operator=(copy_on_write &&x) noexcept
+        {
+            auto tmp = std::move(x);
+            swap(*this, tmp);
+            return *this;
+        }
+
+        element_type &write()
+        {
+            if (!unique()) {
+                *this = copy_on_write(read());
+            }
+            return m_data->value;
+        }
+
+        const element_type &read() const noexcept { return m_data->value; }
+
+        friend inline void swap(copy_on_write &x, copy_on_write &y) noexcept
+        {
+            using std::swap;
+            swap(x.m_data, y.m_data);
+        }
+
+    private:
+        bool unique() const noexcept { return m_data->count == 1; }
+
+    private:
+        payload *m_data;
+};
+
+/**
+ * Specializations for thread-safe code path
+ */
+template<typename T>
+const T &cow_read(const T &v)
+{
+    return v;
+}
+
+template<typename T>
+const T &cow_read(copy_on_write<T> &v)
+{
+    return v.read();
+}
+
+template<typename T>
+T &cow_write(T &v)
+{
+    return v;
+}
+
+template<typename T>
+T &cow_write(copy_on_write<T> &v)
+{
+    return v.write();
+}
+
+/**
+ * std::make_shared instantiates a lot a templates, and makes both compilation time
+ * and executable size far bigger than they need to be. We offer a make_shared
+ * equivalent that will avoid most instantiations with the following tradeoffs:
+ * - Not exception safe,
+ * - Allocates a separate control block, and will thus make the code slower.
+ */
+#ifdef SIGSLOT_REDUCE_COMPILE_TIME
+template<typename B, typename D, typename... Arg>
+inline std::shared_ptr<B> make_shared(Arg &&...arg)
+{
+    return std::shared_ptr<B>(static_cast<B *>(new D(std::forward<Arg>(arg)...)));
+}
+#else
+template<typename B, typename D, typename... Arg>
+inline std::shared_ptr<B> make_shared(Arg &&...arg)
+{
+    return std::static_pointer_cast<B>(std::make_shared<D>(std::forward<Arg>(arg)...));
+}
+#endif
+
+// Adapt a signal into a cheap function object, for easy signal chaining
+template<typename SigT>
+struct signal_wrapper
+{
+        template<typename... U>
+        void operator()(U &&...u)
+        {
+            (*m_sig)(std::forward<U>(u)...);
+        }
+
+        SigT *m_sig{};
+};
+
+/* slot_state holds slot type independent state, to be used to interact with
+ * slots indirectly through connection and scoped_connection objects.
+ */
+class slot_state
+{
+    public:
+        constexpr slot_state(group_id gid) noexcept :
+            m_index(0),
+            m_group(gid),
+            m_connected(true),
+            m_blocked(false)
+        {
+        }
+
+        virtual ~slot_state() = default;
+
+        virtual bool connected() const noexcept { return m_connected; }
+
+        bool disconnect() noexcept
+        {
+            bool ret = m_connected.exchange(false);
+            if (ret) {
+                do_disconnect();
+            }
+            return ret;
+        }
+
+        bool blocked() const noexcept { return m_blocked.load(); }
+        void block() noexcept { m_blocked.store(true); }
+        void unblock() noexcept { m_blocked.store(false); }
+
+    protected:
+        virtual void do_disconnect() { }
+
+        auto index() const { return m_index; }
+
+        auto &index() { return m_index; }
+
+        group_id group() const { return m_group; }
+
+    private:
+        template<typename, typename...>
+        friend class sigslot::signal_base;
+
+        std::size_t m_index;    // index into the array of slot pointers inside the signal
+        const group_id m_group; // slot group this slot belongs to
+        std::atomic<bool> m_connected;
+        std::atomic<bool> m_blocked;
+};
+
+} // namespace detail
+
+/**
+ * connection_blocker is a RAII object that blocks a connection until destruction
+ */
+class connection_blocker
+{
+    public:
+        connection_blocker() = default;
+        ~connection_blocker() noexcept { release(); }
+
+        connection_blocker(const connection_blocker &) = delete;
+        connection_blocker &operator=(const connection_blocker &) = delete;
+
+        connection_blocker(connection_blocker &&o) noexcept :
+            m_state{ std::move(o.m_state) }
+        {
+        }
+
+        connection_blocker &operator=(connection_blocker &&o) noexcept
+        {
+            release();
+            m_state.swap(o.m_state);
+            return *this;
+        }
+
+    private:
+        friend class connection;
+        explicit connection_blocker(std::weak_ptr<detail::slot_state> s) noexcept :
+            m_state{ std::move(s) }
+        {
+            if (auto d = m_state.lock()) {
+                d->block();
+            }
+        }
+
+        void release() noexcept
+        {
+            if (auto d = m_state.lock()) {
+                d->unblock();
+            }
+        }
+
+    private:
+        std::weak_ptr<detail::slot_state> m_state;
+};
+
+/**
+ * A connection object allows interaction with an ongoing slot connection
+ *
+ * It allows common actions such as connection blocking and disconnection.
+ * Note that connection is not a RAII object, one does not need to hold one
+ * such object to keep the signal-slot connection alive.
+ */
+class connection
+{
+    public:
+        connection() = default;
+        virtual ~connection() = default;
+
+        connection(const connection &) noexcept = default;
+        connection &operator=(const connection &) noexcept = default;
+        connection(connection &&) noexcept = default;
+        connection &operator=(connection &&) noexcept = default;
+
+        bool valid() const noexcept { return !m_state.expired(); }
+
+        bool connected() const noexcept
+        {
+            const auto d = m_state.lock();
+            return d && d->connected();
+        }
+
+        bool disconnect() noexcept
+        {
+            auto d = m_state.lock();
+            return d && d->disconnect();
+        }
+
+        bool blocked() const noexcept
+        {
+            const auto d = m_state.lock();
+            return d && d->blocked();
+        }
+
+        void block() noexcept
+        {
+            if (auto d = m_state.lock()) {
+                d->block();
+            }
+        }
+
+        void unblock() noexcept
+        {
+            if (auto d = m_state.lock()) {
+                d->unblock();
+            }
+        }
+
+        connection_blocker blocker() const noexcept { return connection_blocker{ m_state }; }
+
+    protected:
+        template<typename, typename...>
+        friend class signal_base;
+        explicit connection(std::weak_ptr<detail::slot_state> s) noexcept :
+            m_state{ std::move(s) }
+        {
+        }
+
+    protected:
+        std::weak_ptr<detail::slot_state> m_state;
+};
+
+/**
+ * scoped_connection is a RAII version of connection
+ * It disconnects the slot from the signal upon destruction.
+ */
+class scoped_connection final : public connection
+{
+    public:
+        scoped_connection() = default;
+        ~scoped_connection() override { disconnect(); }
+
+        /*implicit*/ scoped_connection(const connection &c) noexcept :
+            connection(c)
+        {
+        }
+        /*implicit*/ scoped_connection(connection &&c) noexcept :
+            connection(std::move(c))
+        {
+        }
+
+        scoped_connection(const scoped_connection &) noexcept = delete;
+        scoped_connection &operator=(const scoped_connection &) noexcept = delete;
+
+        scoped_connection(scoped_connection &&o) noexcept :
+            connection{ std::move(o.m_state) }
+        {
+        }
+
+        scoped_connection &operator=(scoped_connection &&o) noexcept
+        {
+            disconnect();
+            m_state.swap(o.m_state);
+            return *this;
+        }
+
+    private:
+        template<typename, typename...>
+        friend class signal_base;
+        explicit scoped_connection(std::weak_ptr<detail::slot_state> s) noexcept :
+            connection{ std::move(s) }
+        {
+        }
+};
+
+/**
+ * Observer is a base class for intrusive lifetime tracking of objects.
+ *
+ * This is an alternative to trackable pointers, such as std::shared_ptr,
+ * and manual connection management by keeping connection objects in scope.
+ * Deriving from this class allows automatic disconnection of all the slots
+ * connected to any signal when an instance is destroyed.
+ */
+template<typename Lockable>
+struct observer_base : private detail::observer_type
+{
+        virtual ~observer_base() = default;
+
+    protected:
+        /**
+         * Disconnect all signals connected to this object.
+         *
+         * To avoid invocation of slots on a semi-destructed instance, which may happen
+         * in multi-threaded contexts, derived classes should call this method in their
+         * destructor. This will ensure proper disconnection prior to the destruction.
+         */
+        void disconnect_all()
+        {
+            std::unique_lock<Lockable> _{ m_mutex };
+            m_connections.clear();
+        }
+
+    private:
+        template<typename, typename...>
+        friend class signal_base;
+
+        void add_connection(connection conn)
+        {
+            std::unique_lock<Lockable> _{ m_mutex };
+            m_connections.emplace_back(std::move(conn));
+        }
+
+        Lockable m_mutex;
+        std::vector<scoped_connection> m_connections;
+};
+
+/**
+ * Specialization of observer_base to be used in single threaded contexts.
+ */
+using observer_st = observer_base<detail::null_mutex>;
+
+/**
+ * Specialization of observer_base to be used in multi-threaded contexts.
+ */
+using observer = observer_base<std::mutex>;
+
+namespace detail
+{
+
+// interface for cleanable objects, used to cleanup disconnected slots
+struct cleanable
+{
+        virtual ~cleanable() = default;
+        virtual void clean(slot_state *) = 0;
+};
+
+template<typename...>
+class slot_base;
+
+template<typename... T>
+using slot_ptr = std::shared_ptr<slot_base<T...>>;
+
+/* A base class for slot objects. This base type only depends on slot argument
+ * types, it will be used as an element in an intrusive singly-linked list of
+ * slots, hence the public next member.
+ */
+template<typename... Args>
+class slot_base : public slot_state
+{
+    public:
+        using base_types = trait::typelist<Args...>;
+
+        explicit slot_base(cleanable &c, group_id gid) :
+            slot_state(gid),
+            cleaner(c)
+        {
+        }
+        ~slot_base() override = default;
+
+        // method effectively responsible for calling the "slot" function with
+        // supplied arguments whenever emission happens.
+        virtual void call_slot(Args...) = 0;
+
+        template<typename... U>
+        void operator()(U &&...u)
+        {
+            if (slot_state::connected() && !slot_state::blocked()) {
+                call_slot(std::forward<U>(u)...);
+            }
+        }
+
+        // check if we are storing callable c
+        template<typename C>
+        bool has_callable(const C &c) const
+        {
+            auto p = get_callable();
+            return eq_function_ptr(c, p);
+        }
+
+        template<typename C>
+        std::enable_if_t<function_traits<C>::must_check_object, bool> has_full_callable(const C &c) const
+        {
+            return has_callable(c) && check_class_type<std::decay_t<C>>();
+        }
+
+        template<typename C>
+        std::enable_if_t<!function_traits<C>::must_check_object, bool> has_full_callable(const C &c) const
+        {
+            return has_callable(c);
+        }
+
+        // check if we are storing object o
+        template<typename O>
+        bool has_object(const O &o) const
+        {
+            return get_object() == get_object_ptr(o);
+        }
+
+    protected:
+        void do_disconnect() final { cleaner.clean(this); }
+
+        // retieve a pointer to the object embedded in the slot
+        virtual obj_ptr get_object() const noexcept { return nullptr; }
+
+        // retieve a pointer to the callable embedded in the slot
+        virtual func_ptr get_callable() const noexcept { return get_function_ptr(nullptr); }
+
+#ifdef SIGSLOT_RTTI_ENABLED
+        // retieve a pointer to the callable embedded in the slot
+        virtual const std::type_info &get_callable_type() const noexcept
+        {
+            return typeid(nullptr);
+        }
+
+    private:
+        template<typename U>
+        bool check_class_type() const
+        {
+            return typeid(U) == get_callable_type();
+        }
+
+#else
+        template<typename U>
+        bool check_class_type() const
+        {
+            return false;
+        }
+#endif
+
+    private:
+        cleanable &cleaner;
+};
+
+/*
+ * A slot object holds state information, and a callable to to be called
+ * whenever the function call operator of its slot_base base class is called.
+ */
+template<typename Func, typename... Args>
+class slot final : public slot_base<Args...>
+{
+    public:
+        template<typename F, typename Gid>
+        constexpr slot(cleanable &c, F &&f, Gid gid) :
+            slot_base<Args...>(c, gid),
+            func{ std::forward<F>(f) }
+        {
+        }
+
+    protected:
+        void call_slot(Args... args) override { func(args...); }
+
+        func_ptr get_callable() const noexcept override { return get_function_ptr(func); }
+
+#ifdef SIGSLOT_RTTI_ENABLED
+        const std::type_info &get_callable_type() const noexcept override
+        {
+            return typeid(func);
+        }
+#endif
+
+    private:
+        std::decay_t<Func> func;
+};
+
+/*
+ * Variation of slot that prepends a connection object to the callable
+ */
+template<typename Func, typename... Args>
+class slot_extended final : public slot_base<Args...>
+{
+    public:
+        template<typename F>
+        constexpr slot_extended(cleanable &c, F &&f, group_id gid) :
+            slot_base<Args...>(c, gid),
+            func{ std::forward<F>(f) }
+        {
+        }
+
+        connection conn;
+
+    protected:
+        void call_slot(Args... args) override { func(conn, args...); }
+
+        func_ptr get_callable() const noexcept override { return get_function_ptr(func); }
+
+#ifdef SIGSLOT_RTTI_ENABLED
+        const std::type_info &get_callable_type() const noexcept override
+        {
+            return typeid(func);
+        }
+#endif
+
+    private:
+        std::decay_t<Func> func;
+};
+
+/*
+ * A slot object holds state information, an object and a pointer over member
+ * function to be called whenever the function call operator of its slot_base
+ * base class is called.
+ */
+template<typename Pmf, typename Ptr, typename... Args>
+class slot_pmf final : public slot_base<Args...>
+{
+    public:
+        template<typename F, typename P>
+        constexpr slot_pmf(cleanable &c, F &&f, P &&p, group_id gid) :
+            slot_base<Args...>(c, gid),
+            pmf{ std::forward<F>(f) },
+            ptr{ std::forward<P>(p) }
+        {
+        }
+
+    protected:
+        void call_slot(Args... args) override { ((*ptr).*pmf)(args...); }
+
+        func_ptr get_callable() const noexcept override { return get_function_ptr(pmf); }
+
+        obj_ptr get_object() const noexcept override { return get_object_ptr(ptr); }
+
+#ifdef SIGSLOT_RTTI_ENABLED
+        const std::type_info &get_callable_type() const noexcept override
+        {
+            return typeid(pmf);
+        }
+#endif
+
+    private:
+        std::decay_t<Pmf> pmf;
+        std::decay_t<Ptr> ptr;
+};
+
+/*
+ * Variation of slot that prepends a connection object to the callable
+ */
+template<typename Pmf, typename Ptr, typename... Args>
+class slot_pmf_extended final : public slot_base<Args...>
+{
+    public:
+        template<typename F, typename P>
+        constexpr slot_pmf_extended(cleanable &c, F &&f, P &&p, group_id gid) :
+            slot_base<Args...>(c, gid),
+            pmf{ std::forward<F>(f) },
+            ptr{ std::forward<P>(p) }
+        {
+        }
+
+        connection conn;
+
+    protected:
+        void call_slot(Args... args) override { ((*ptr).*pmf)(conn, args...); }
+
+        func_ptr get_callable() const noexcept override { return get_function_ptr(pmf); }
+        obj_ptr get_object() const noexcept override { return get_object_ptr(ptr); }
+
+#ifdef SIGSLOT_RTTI_ENABLED
+        const std::type_info &get_callable_type() const noexcept override
+        {
+            return typeid(pmf);
+        }
+#endif
+
+    private:
+        std::decay_t<Pmf> pmf;
+        std::decay_t<Ptr> ptr;
+};
+
+/*
+ * An implementation of a slot that tracks the life of a supplied object
+ * through a weak pointer in order to automatically disconnect the slot
+ * on said object destruction.
+ */
+template<typename Func, typename WeakPtr, typename... Args>
+class slot_tracked final : public slot_base<Args...>
+{
+    public:
+        template<typename F, typename P>
+        constexpr slot_tracked(cleanable &c, F &&f, P &&p, group_id gid) :
+            slot_base<Args...>(c, gid),
+            func{ std::forward<F>(f) },
+            ptr{ std::forward<P>(p) }
+        {
+        }
+
+        bool connected() const noexcept override { return !ptr.expired() && slot_state::connected(); }
+
+    protected:
+        void call_slot(Args... args) override
+        {
+            auto sp = ptr.lock();
+            if (!sp) {
+                slot_state::disconnect();
+                return;
+            }
+            if (slot_state::connected()) {
+                func(args...);
+            }
+        }
+
+        func_ptr get_callable() const noexcept override { return get_function_ptr(func); }
+
+        obj_ptr get_object() const noexcept override { return get_object_ptr(ptr); }
+
+#ifdef SIGSLOT_RTTI_ENABLED
+        const std::type_info &get_callable_type() const noexcept override
+        {
+            return typeid(func);
+        }
+#endif
+
+    private:
+        std::decay_t<Func> func;
+        std::decay_t<WeakPtr> ptr;
+};
+
+/*
+ * An implementation of a slot as a pointer over member function, that tracks
+ * the life of a supplied object through a weak pointer in order to automatically
+ * disconnect the slot on said object destruction.
+ */
+template<typename Pmf, typename WeakPtr, typename... Args>
+class slot_pmf_tracked final : public slot_base<Args...>
+{
+    public:
+        template<typename F, typename P>
+        constexpr slot_pmf_tracked(cleanable &c, F &&f, P &&p, group_id gid) :
+            slot_base<Args...>(c, gid),
+            pmf{ std::forward<F>(f) },
+            ptr{ std::forward<P>(p) }
+        {
+        }
+
+        bool connected() const noexcept override { return !ptr.expired() && slot_state::connected(); }
+
+    protected:
+        void call_slot(Args... args) override
+        {
+            auto sp = ptr.lock();
+            if (!sp) {
+                slot_state::disconnect();
+                return;
+            }
+            if (slot_state::connected()) {
+                ((*sp).*pmf)(args...);
+            }
+        }
+
+        func_ptr get_callable() const noexcept override { return get_function_ptr(pmf); }
+
+        obj_ptr get_object() const noexcept override { return get_object_ptr(ptr); }
+
+#ifdef SIGSLOT_RTTI_ENABLED
+        const std::type_info &get_callable_type() const noexcept override
+        {
+            return typeid(pmf);
+        }
+#endif
+
+    private:
+        std::decay_t<Pmf> pmf;
+        std::decay_t<WeakPtr> ptr;
+};
+
+} // namespace detail
+
+/**
+ * signal_base is an implementation of the observer pattern, through the use
+ * of an emitting object and slots that are connected to the signal and called
+ * with supplied arguments when a signal is emitted.
+ *
+ * signal_base is the general implementation, whose locking policy must be
+ * set in order to decide thread safety guarantees. signal and signal_st
+ * are partial specializations for multi-threaded and single-threaded use.
+ *
+ * It does not allow slots to return a value.
+ *
+ * Slot execution order can be constrained by assigning group ids to the slots.
+ * The execution order of slots in a same group is unspecified and should not be
+ * relied upon, however groups are executed in ascending group ids order. When
+ * the group id of a slot is not set, it is assigned to the group 0. Group ids
+ * can have any value in the range of signed 32 bit integers.
+ *
+ * @tparam Lockable a lock type to decide the lock policy
+ * @tparam T... the argument types of the emitting and slots functions.
+ */
+template<typename Lockable, typename... T>
+class signal_base final : public detail::cleanable
+{
+        template<typename L>
+        using is_thread_safe = std::integral_constant<bool, !std::is_same<L, detail::null_mutex>::value>;
+
+        template<typename U, typename L>
+        using cow_type = std::conditional_t<is_thread_safe<L>::value, detail::copy_on_write<U>, U>;
+
+        template<typename U, typename L>
+        using cow_copy_type = std::conditional_t<is_thread_safe<L>::value, detail::copy_on_write<U>, const U &>;
+
+        using lock_type = std::unique_lock<Lockable>;
+        using slot_base = detail::slot_base<T...>;
+        using slot_ptr = detail::slot_ptr<T...>;
+        using slots_type = std::vector<slot_ptr>;
+        struct group_type
+        {
+                slots_type slts;
+                group_id gid;
+        };
+        using list_type = std::vector<group_type>; // kept ordered by ascending gid
+
+    public:
+        using arg_list = trait::typelist<T...>;
+        using ext_arg_list = trait::typelist<connection &, T...>;
+
+        signal_base() noexcept :
+            m_block(false)
+        {
+        }
+        ~signal_base() override { disconnect_all(); }
+
+        signal_base(const signal_base &) = delete;
+        signal_base &operator=(const signal_base &) = delete;
+
+        signal_base(signal_base &&o) /* not noexcept */
+            :
+            m_block{ o.m_block.load() }
+        {
+            lock_type lock(o.m_mutex);
+            using std::swap;
+            swap(m_slots, o.m_slots);
+        }
+
+        signal_base &operator=(signal_base &&o) /* not noexcept */
+        {
+            lock_type lock1(m_mutex, std::defer_lock);
+            lock_type lock2(o.m_mutex, std::defer_lock);
+            std::lock(lock1, lock2);
+
+            using std::swap;
+            swap(m_slots, o.m_slots);
+            m_block.store(o.m_block.exchange(m_block.load()));
+            return *this;
+        }
+
+        /**
+         * Emit a signal
+         *
+         * Effect: All non blocked and connected slot functions will be called
+         *         with supplied arguments.
+         * Safety: With proper locking (see pal::signal), emission can happen from
+         *         multiple threads simultaneously. The guarantees only apply to the
+         *         signal object, it does not cover thread safety of potentially
+         *         shared state used in slot functions.
+         *
+         * @param a... arguments to emit
+         */
+        template<typename... U>
+        void operator()(U &&...a)
+        {
+            if (m_block) {
+                return;
+            }
+
+            // Reference to the slots to execute them out of the lock
+            // a copy may occur if another thread writes to it.
+            cow_copy_type<list_type, Lockable> ref = slots_reference();
+
+            for (const auto &group : detail::cow_read(ref)) {
+                for (const auto &s : group.slts) {
+                    s->operator()(a...);
+                }
+            }
+        }
+
+        /**
+         * Connect a callable of compatible arguments
+         *
+         * Effect: Creates and stores a new slot responsible for executing the
+         *         supplied callable for every subsequent signal emission.
+         * Safety: Thread-safety depends on locking policy.
+         *
+         * @param c a callable
+         * @param gid an identifier that can be used to order slot execution
+         * @return a connection object that can be used to interact with the slot
+         */
+        template<typename Callable>
+        std::enable_if_t<trait::is_callable_v<arg_list, Callable>, connection> connect(Callable &&c, group_id gid = 0)
+        {
+            using slot_t = detail::slot<Callable, T...>;
+            auto s = make_slot<slot_t>(std::forward<Callable>(c), gid);
+            connection conn(s);
+            add_slot(std::move(s));
+            return conn;
+        }
+
+        /**
+         * Connect a callable with an additional connection argument
+         *
+         * The callable's first argument must be of type connection. This overload
+         * the callable to manage it's own connection through this argument.
+         *
+         * @param c a callable
+         * @param gid an identifier that can be used to order slot execution
+         * @return a connection object that can be used to interact with the slot
+         */
+        template<typename Callable>
+        std::enable_if_t<trait::is_callable_v<ext_arg_list, Callable>, connection> connect_extended(Callable &&c, group_id gid = 0)
+        {
+            using slot_t = detail::slot_extended<Callable, T...>;
+            auto s = make_slot<slot_t>(std::forward<Callable>(c), gid);
+            connection conn(s);
+            std::static_pointer_cast<slot_t>(s)->conn = conn;
+            add_slot(std::move(s));
+            return conn;
+        }
+
+        /**
+         * Overload of connect for pointers over member functions derived from
+         * observer
+         *
+         * @param pmf a pointer over member function
+         * @param ptr an object pointer derived from observer
+         * @param gid an identifier that can be used to order slot execution
+         * @return a connection object that can be used to interact with the slot
+         */
+        template<typename Pmf, typename Ptr>
+        std::enable_if_t<trait::is_callable_v<arg_list, Pmf, Ptr> && trait::is_observer_v<Ptr>, connection> connect(Pmf &&pmf, Ptr &&ptr, group_id gid = 0)
+        {
+            using slot_t = detail::slot_pmf<Pmf, Ptr, T...>;
+            auto s = make_slot<slot_t>(std::forward<Pmf>(pmf), std::forward<Ptr>(ptr), gid);
+            connection conn(s);
+            add_slot(std::move(s));
+            ptr->add_connection(conn);
+            return conn;
+        }
+
+        /**
+         * Overload of connect for pointers over member functions
+         *
+         * @param pmf a pointer over member function
+         * @param ptr an object pointer
+         * @param gid an identifier that can be used to order slot execution
+         * @return a connection object that can be used to interact with the slot
+         */
+        template<typename Pmf, typename Ptr>
+        std::enable_if_t<trait::is_callable_v<arg_list, Pmf, Ptr> && !trait::is_observer_v<Ptr> && !trait::is_weak_ptr_compatible_v<Ptr>, connection> connect(Pmf &&pmf, Ptr &&ptr, group_id gid = 0)
+        {
+            using slot_t = detail::slot_pmf<Pmf, Ptr, T...>;
+            auto s = make_slot<slot_t>(std::forward<Pmf>(pmf), std::forward<Ptr>(ptr), gid);
+            connection conn(s);
+            add_slot(std::move(s));
+            return conn;
+        }
+
+        /**
+         * Overload  of connect for pointer over member functions and
+         *
+         * @param pmf a pointer over member function
+         * @param ptr an object pointer
+         * @param gid an identifier that can be used to order slot execution
+         * @return a connection object that can be used to interact with the slot
+         */
+        template<typename Pmf, typename Ptr>
+        std::enable_if_t<trait::is_callable_v<ext_arg_list, Pmf, Ptr> && !trait::is_weak_ptr_compatible_v<Ptr>, connection> connect_extended(Pmf &&pmf, Ptr &&ptr, group_id gid = 0)
+        {
+            using slot_t = detail::slot_pmf_extended<Pmf, Ptr, T...>;
+            auto s = make_slot<slot_t>(std::forward<Pmf>(pmf), std::forward<Ptr>(ptr), gid);
+            connection conn(s);
+            std::static_pointer_cast<slot_t>(s)->conn = conn;
+            add_slot(std::move(s));
+            return conn;
+        }
+
+        /**
+         * Overload of connect for lifetime object tracking and automatic disconnection
+         *
+         * Ptr must be convertible to an object following a loose form of weak pointer
+         * concept, by implementing the ADL-detected conversion function to_weak().
+         *
+         * This overload covers the case of a pointer over member function and a
+         * trackable pointer of that class.
+         *
+         * Note: only weak references are stored, a slot does not extend the lifetime
+         * of a suppied object.
+         *
+         * @param pmf a pointer over member function
+         * @param ptr a trackable object pointer
+         * @param gid an identifier that can be used to order slot execution
+         * @return a connection object that can be used to interact with the slot
+         */
+        template<typename Pmf, typename Ptr>
+        std::enable_if_t<!trait::is_callable_v<arg_list, Pmf> && trait::is_weak_ptr_compatible_v<Ptr>, connection> connect(Pmf &&pmf, Ptr &&ptr, group_id gid = 0)
+        {
+            using trait::to_weak;
+            auto w = to_weak(std::forward<Ptr>(ptr));
+            using slot_t = detail::slot_pmf_tracked<Pmf, decltype(w), T...>;
+            auto s = make_slot<slot_t>(std::forward<Pmf>(pmf), w, gid);
+            connection conn(s);
+            add_slot(std::move(s));
+            return conn;
+        }
+
+        /**
+         * Overload of connect for lifetime object tracking and automatic disconnection
+         *
+         * Trackable must be convertible to an object following a loose form of weak
+         * pointer concept, by implementing the ADL-detected conversion function to_weak().
+         *
+         * This overload covers the case of a standalone callable and unrelated trackable
+         * object.
+         *
+         * Note: only weak references are stored, a slot does not extend the lifetime
+         * of a suppied object.
+         *
+         * @param c a callable
+         * @param ptr a trackable object pointer
+         * @param gid an identifier that can be used to order slot execution
+         * @return a connection object that can be used to interact with the slot
+         */
+        template<typename Callable, typename Trackable>
+        std::enable_if_t<trait::is_callable_v<arg_list, Callable> && trait::is_weak_ptr_compatible_v<Trackable>, connection> connect(Callable &&c, Trackable &&ptr, group_id gid = 0)
+        {
+            using trait::to_weak;
+            auto w = to_weak(std::forward<Trackable>(ptr));
+            using slot_t = detail::slot_tracked<Callable, decltype(w), T...>;
+            auto s = make_slot<slot_t>(std::forward<Callable>(c), w, gid);
+            connection conn(s);
+            add_slot(std::move(s));
+            return conn;
+        }
+
+        /**
+         * Creates a connection whose duration is tied to the return object
+         * Use the same semantics as connect
+         */
+        template<typename... CallArgs>
+        scoped_connection connect_scoped(CallArgs &&...args)
+        {
+            return connect(std::forward<CallArgs>(args)...);
+        }
+
+        /**
+         * Disconnect slots bound to a callable
+         *
+         * Effect: Disconnects all the slots bound to the callable in argument.
+         * Safety: Thread-safety depends on locking policy.
+         *
+         * If the callable is a free or static member function, this overload is always
+         * available. However, RTTI is needed for it to work for pointer to member
+         * functions, function objects or and (references to) lambdas, because the
+         * C++ spec does not mandate the pointers to member functions to be unique.
+         *
+         * @param c a callable
+         * @return the number of disconnected slots
+         */
+        template<typename Callable>
+        std::enable_if_t<
+            (trait::is_callable_v<arg_list, Callable> || trait::is_callable_v<ext_arg_list, Callable> || trait::is_pmf_v<Callable>)&&detail::function_traits<Callable>::is_disconnectable,
+            size_t>
+        disconnect(const Callable &c)
+        {
+            return disconnect_if([&](const auto &s) { return s->has_full_callable(c); });
+        }
+
+        /**
+         * Disconnect slots bound to this object
+         *
+         * Effect: Disconnects all the slots bound to the object or tracked object
+         *         in argument.
+         * Safety: Thread-safety depends on locking policy.
+         *
+         * The object may be a pointer or trackable object.
+         *
+         * @param obj an object
+         * @return the number of disconnected slots
+         */
+        template<typename Obj>
+        std::enable_if_t<!trait::is_callable_v<arg_list, Obj> && !trait::is_callable_v<ext_arg_list, Obj> && !trait::is_pmf_v<Obj>, size_t> disconnect(const Obj &obj)
+        {
+            return disconnect_if([&](const auto &s) { return s->has_object(obj); });
+        }
+
+        /**
+         * Disconnect slots bound both to a callable and object
+         *
+         * Effect: Disconnects all the slots bound to the callable and object in argument.
+         * Safety: Thread-safety depends on locking policy.
+         *
+         * For naked pointers, the Callable is expected to be a pointer over member
+         * function. If obj is trackable, any kind of Callable can be used.
+         *
+         * @param c a callable
+         * @param obj an object
+         * @return the number of disconnected slots
+         */
+        template<typename Callable, typename Obj>
+        size_t disconnect(const Callable &c, const Obj &obj)
+        {
+            return disconnect_if([&](const auto &s) { return s->has_object(obj) && s->has_callable(c); });
+        }
+
+        /**
+         * Disconnect slots in a particular group
+         *
+         * Effect: Disconnects all the slots in the group id in argument.
+         * Safety: Thread-safety depends on locking policy.
+         *
+         * @param gid a group id
+         * @return the number of disconnected slots
+         */
+        size_t disconnect(group_id gid)
+        {
+            lock_type lock(m_mutex);
+            for (auto &group : detail::cow_write(m_slots)) {
+                if (group.gid == gid) {
+                    size_t count = group.slts.size();
+                    group.slts.clear();
+                    return count;
+                }
+            }
+            return 0;
+        }
+
+        /**
+         * Disconnects all the slots
+         * Safety: Thread safety depends on locking policy
+         */
+        void disconnect_all()
+        {
+            lock_type lock(m_mutex);
+            clear();
+        }
+
+        /**
+         * Blocks signal emission
+         * Safety: thread safe
+         */
+        void block() noexcept { m_block.store(true); }
+
+        /**
+         * Unblocks signal emission
+         * Safety: thread safe
+         */
+        void unblock() noexcept { m_block.store(false); }
+
+        /**
+         * Tests blocking state of signal emission
+         */
+        bool blocked() const noexcept { return m_block.load(); }
+
+        /**
+         * Get number of connected slots
+         * Safety: thread safe
+         */
+        size_t slot_count() noexcept
+        {
+            cow_copy_type<list_type, Lockable> ref = slots_reference();
+            size_t count = 0;
+            for (const auto &g : detail::cow_read(ref)) {
+                count += g.slts.size();
+            }
+            return count;
+        }
+
+    protected:
+        /**
+         * remove disconnected slots
+         */
+        void clean(detail::slot_state *state) override
+        {
+            lock_type lock(m_mutex);
+            const auto idx = state->index();
+            const auto gid = state->group();
+
+            // find the group
+            for (auto &group : detail::cow_write(m_slots)) {
+                if (group.gid == gid) {
+                    auto &slts = group.slts;
+
+                    // ensure we have the right slot, in case of concurrent cleaning
+                    if (idx < slts.size() && slts[idx] && slts[idx].get() == state) {
+                        std::swap(slts[idx], slts.back());
+                        slts[idx]->index() = idx;
+                        slts.pop_back();
+                    }
+
+                    return;
+                }
+            }
+        }
+
+    private:
+        // used to get a reference to the slots for reading
+        inline cow_copy_type<list_type, Lockable> slots_reference()
+        {
+            lock_type lock(m_mutex);
+            return m_slots;
+        }
+
+        // create a new slot
+        template<typename Slot, typename... A>
+        inline auto make_slot(A &&...a)
+        {
+            return detail::make_shared<slot_base, Slot>(*this, std::forward<A>(a)...);
+        }
+
+        // add the slot to the list of slots of the right group
+        void add_slot(slot_ptr &&s)
+        {
+            const group_id gid = s->group();
+
+            lock_type lock(m_mutex);
+            auto &groups = detail::cow_write(m_slots);
+
+            // find the group
+            auto it = groups.begin();
+            while (it != groups.end() && it->gid < gid) {
+                it++;
+            }
+
+            // create a new group if necessary
+            if (it == groups.end() || it->gid != gid) {
+                it = groups.insert(it, { {}, gid });
+            }
+
+            // add the slot
+            s->index() = it->slts.size();
+            it->slts.push_back(std::move(s));
+        }
+
+        // disconnect a slot if a condition occurs
+        template<typename Cond>
+        size_t disconnect_if(Cond &&cond)
+        {
+            lock_type lock(m_mutex);
+            auto &groups = detail::cow_write(m_slots);
+
+            size_t count = 0;
+
+            for (auto &group : groups) {
+                auto &slts = group.slts;
+                size_t i = 0;
+                while (i < slts.size()) {
+                    if (cond(slts[i])) {
+                        std::swap(slts[i], slts.back());
+                        slts[i]->index() = i;
+                        slts.pop_back();
+                        ++count;
+                    } else {
+                        ++i;
+                    }
+                }
+            }
+
+            return count;
+        }
+
+        // to be called under lock: remove all the slots
+        void clear() { detail::cow_write(m_slots).clear(); }
+
+    private:
+        Lockable m_mutex;
+        cow_type<list_type, Lockable> m_slots;
+        std::atomic<bool> m_block;
+};
+
+/**
+ * Freestanding connect function that defers to the `signal_base::connect` member.
+ */
+template<typename Lockable, typename Arg, typename... T, typename... Args>
+std::enable_if_t<!trait::is_signal_v<std::decay_t<Arg>>, connection> connect(signal_base<Lockable, T...> &sig, Arg &&arg, Args &&...args)
+{
+    return sig.connect(std::forward<Arg>(arg), std::forward<Args>(args)...);
+}
+
+/**
+ * Freestanding connect function that chains one signal to another.
+ */
+template<typename Lockable1, typename Lockable2, typename... T1, typename... T2, typename... Args>
+connection connect(signal_base<Lockable1, T1...> &sig1, signal_base<Lockable2, T2...> &sig2, Args &&...args)
+{
+    return sig1.connect(detail::signal_wrapper<signal_base<Lockable2, T2...>>{ std::addressof(sig2) }, std::forward<Args>(args)...);
+}
+
+/**
+ * Specialization of signal_base to be used in single threaded contexts.
+ * Slot connection, disconnection and signal emission are not thread-safe.
+ * The performance improvement over the thread-safe variant is not impressive,
+ * so this is not very useful.
+ */
+template<typename... T>
+using signal_st = signal_base<detail::null_mutex, T...>;
+
+/**
+ * Specialization of signal_base to be used in multi-threaded contexts.
+ * Slot connection, disconnection and signal emission are thread-safe.
+ *
+ * Recursive signal emission and emission cycles are supported too.
+ */
+template<typename... T>
+using signal = signal_base<std::mutex, T...>;
+
+} // namespace libscratchcpp::sigslot

From 30279211edaece7c48043dfb9b2b4d28b5b7b2bb Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Thu, 21 Mar 2024 16:32:13 +0100
Subject: [PATCH 41/77] Replace redraw handler with a signal

---
 include/scratchcpp/iengine.h   | 5 +++--
 src/engine/internal/engine.cpp | 7 +++----
 src/engine/internal/engine.h   | 4 ++--
 test/engine/engine_test.cpp    | 2 +-
 test/mocks/enginemock.h        | 2 +-
 5 files changed, 10 insertions(+), 10 deletions(-)

diff --git a/include/scratchcpp/iengine.h b/include/scratchcpp/iengine.h
index 987fba22..1370a346 100644
--- a/include/scratchcpp/iengine.h
+++ b/include/scratchcpp/iengine.h
@@ -8,6 +8,7 @@
 #include <functional>
 
 #include "global.h"
+#include "signal.h"
 
 namespace libscratchcpp
 {
@@ -109,8 +110,8 @@ class LIBSCRATCHCPP_EXPORT IEngine
         /*! Stops the event loop which is running in another thread. */
         virtual void stopEventLoop() = 0;
 
-        /*! Sets the function which is called on every frame. */
-        virtual void setRedrawHandler(const std::function<void()> &handler) = 0;
+        /*! Emits when rendering should occur. */
+        virtual sigslot::signal<> &aboutToRender() = 0;
 
         /*! Returns true if the project is currently running. */
         virtual bool isRunning() const = 0;
diff --git a/src/engine/internal/engine.cpp b/src/engine/internal/engine.cpp
index 1ad56431..de4c5346 100644
--- a/src/engine/internal/engine.cpp
+++ b/src/engine/internal/engine.cpp
@@ -486,8 +486,7 @@ void Engine::step()
     stepThreads();
 
     // Render
-    if (m_redrawHandler)
-        m_redrawHandler();
+    m_aboutToRedraw();
 }
 
 void Engine::run()
@@ -509,9 +508,9 @@ void Engine::stopEventLoop()
     m_stopEventLoopMutex.unlock();
 }
 
-void Engine::setRedrawHandler(const std::function<void()> &handler)
+sigslot::signal<> &Engine::aboutToRender()
 {
-    m_redrawHandler = handler;
+    return m_aboutToRedraw;
 }
 
 std::vector<std::shared_ptr<VirtualMachine>> Engine::stepThreads()
diff --git a/src/engine/internal/engine.h b/src/engine/internal/engine.h
index d73cc196..f0f627a0 100644
--- a/src/engine/internal/engine.h
+++ b/src/engine/internal/engine.h
@@ -49,7 +49,7 @@ class Engine : public IEngine
         void runEventLoop() override;
         void stopEventLoop() override;
 
-        void setRedrawHandler(const std::function<void()> &handler) override;
+        sigslot::signal<> &aboutToRender() override;
 
         bool isRunning() const override;
 
@@ -268,7 +268,7 @@ class Engine : public IEngine
 
         bool m_running = false;
         bool m_redrawRequested = false;
-        std::function<void()> m_redrawHandler = nullptr;
+        sigslot::signal<> m_aboutToRedraw;
         bool m_stopEventLoop = false;
         std::mutex m_stopEventLoopMutex;
 
diff --git a/test/engine/engine_test.cpp b/test/engine/engine_test.cpp
index 1e44ec6f..006f90d0 100644
--- a/test/engine/engine_test.cpp
+++ b/test/engine/engine_test.cpp
@@ -341,7 +341,7 @@ TEST(EngineTest, FpsProject)
     engine->setFps(10);
     RedrawMock redrawMock;
     auto handler = std::bind(&RedrawMock::redraw, &redrawMock);
-    engine->setRedrawHandler(std::function<void()>(handler));
+    engine->aboutToRender().connect(handler);
     std::chrono::steady_clock::time_point time7(std::chrono::milliseconds(100));
     std::chrono::steady_clock::time_point time8(std::chrono::milliseconds(200));
     std::chrono::steady_clock::time_point time9(std::chrono::milliseconds(300));
diff --git a/test/mocks/enginemock.h b/test/mocks/enginemock.h
index be7a2aa3..7ee6c02f 100644
--- a/test/mocks/enginemock.h
+++ b/test/mocks/enginemock.h
@@ -32,7 +32,7 @@ class EngineMock : public IEngine
         MOCK_METHOD(void, runEventLoop, (), (override));
         MOCK_METHOD(void, stopEventLoop, (), (override));
 
-        MOCK_METHOD(void, setRedrawHandler, (const std::function<void()> &), (override));
+        MOCK_METHOD(sigslot::signal<> &, aboutToRender, (), (override));
 
         MOCK_METHOD(bool, isRunning, (), (const, override));
 

From 9949e33a976c1a98f861f1fbcc7572028c3f2a5b Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Thu, 21 Mar 2024 16:40:30 +0100
Subject: [PATCH 42/77] Replace monitor added/removed handlers with signals

---
 include/scratchcpp/iengine.h   |  8 ++++----
 src/engine/internal/engine.cpp | 31 ++++++++++++-------------------
 src/engine/internal/engine.h   |  8 ++++----
 test/engine/engine_test.cpp    |  6 +++---
 test/mocks/enginemock.h        |  4 ++--
 5 files changed, 25 insertions(+), 32 deletions(-)

diff --git a/include/scratchcpp/iengine.h b/include/scratchcpp/iengine.h
index 1370a346..f50d864c 100644
--- a/include/scratchcpp/iengine.h
+++ b/include/scratchcpp/iengine.h
@@ -346,11 +346,11 @@ class LIBSCRATCHCPP_EXPORT IEngine
         /*! Sets the list of monitors. */
         virtual void setMonitors(const std::vector<std::shared_ptr<Monitor>> &newMonitors) = 0;
 
-        /*! Sets the function which is called when a monitor is added. */
-        virtual void setAddMonitorHandler(const std::function<void(Monitor *)> &handler) = 0;
+        /*! Emits when a monitor is added. */
+        virtual sigslot::signal<Monitor *> &monitorAdded() = 0;
 
-        /*! Sets the function which is called when a monitor is removed. */
-        virtual void setRemoveMonitorHandler(const std::function<void(Monitor *, IMonitorHandler *)> &handler) = 0;
+        /*! Emits when a monitor is removed. */
+        virtual sigslot::signal<Monitor *, IMonitorHandler *> &monitorRemoved() = 0;
 
         /*! Returns the function which is called when a question is asked, for example using the 'ask and wait' block. */
         virtual const std::function<void(const std::string &)> &questionAsked() const = 0;
diff --git a/src/engine/internal/engine.cpp b/src/engine/internal/engine.cpp
index de4c5346..a63cd2b4 100644
--- a/src/engine/internal/engine.cpp
+++ b/src/engine/internal/engine.cpp
@@ -60,10 +60,8 @@ void Engine::clear()
 {
     stop();
 
-    if (m_removeMonitorHandler) {
-        for (auto monitor : m_monitors)
-            m_removeMonitorHandler(monitor.get(), monitor->impl->iface);
-    }
+    for (auto monitor : m_monitors)
+        m_monitorRemoved(monitor.get(), monitor->impl->iface);
 
     m_sections.clear();
     m_targets.clear();
@@ -1220,28 +1218,25 @@ const std::vector<std::shared_ptr<Monitor>> &Engine::monitors() const
 
 void Engine::setMonitors(const std::vector<std::shared_ptr<Monitor>> &newMonitors)
 {
-    if (m_addMonitorHandler) {
-        m_monitors.clear();
+    m_monitors.clear();
 
-        for (auto monitor : newMonitors) {
-            m_monitors.push_back(monitor);
-            m_addMonitorHandler(monitor.get());
-        }
-    } else
-        m_monitors = newMonitors;
+    for (auto monitor : newMonitors) {
+        m_monitors.push_back(monitor);
+        m_monitorAdded(monitor.get());
+    }
 
     // Create missing monitors
     createMissingMonitors();
 }
 
-void Engine::setAddMonitorHandler(const std::function<void(Monitor *)> &handler)
+sigslot::signal<Monitor *> &Engine::monitorAdded()
 {
-    m_addMonitorHandler = handler;
+    return m_monitorAdded;
 }
 
-void Engine::setRemoveMonitorHandler(const std::function<void(Monitor *, IMonitorHandler *)> &handler)
+sigslot::signal<Monitor *, IMonitorHandler *> &Engine::monitorRemoved()
 {
-    m_removeMonitorHandler = handler;
+    return m_monitorRemoved;
 }
 
 const std::function<void(const std::string &)> &Engine::questionAsked() const
@@ -1633,9 +1628,7 @@ void Engine::addVarOrListMonitor(std::shared_ptr<Monitor> monitor, Target *targe
     monitor->setY(rect.top());
 
     m_monitors.push_back(monitor);
-
-    if (m_addMonitorHandler)
-        m_addMonitorHandler(monitor.get());
+    m_monitorAdded(monitor.get());
 
     monitor->setVisible(false);
 }
diff --git a/src/engine/internal/engine.h b/src/engine/internal/engine.h
index f0f627a0..79b4fb88 100644
--- a/src/engine/internal/engine.h
+++ b/src/engine/internal/engine.h
@@ -143,8 +143,8 @@ class Engine : public IEngine
 
         const std::vector<std::shared_ptr<Monitor>> &monitors() const override;
         void setMonitors(const std::vector<std::shared_ptr<Monitor>> &newMonitors) override;
-        void setAddMonitorHandler(const std::function<void(Monitor *)> &handler) override;
-        void setRemoveMonitorHandler(const std::function<void(Monitor *, IMonitorHandler *)> &handler) override;
+        sigslot::signal<Monitor *> &monitorAdded() override;
+        sigslot::signal<Monitor *, IMonitorHandler *> &monitorRemoved() override;
 
         const std::function<void(const std::string &)> &questionAsked() const override;
         void setQuestionAsked(const std::function<void(const std::string &)> &f) override;
@@ -272,8 +272,8 @@ class Engine : public IEngine
         bool m_stopEventLoop = false;
         std::mutex m_stopEventLoopMutex;
 
-        std::function<void(Monitor *)> m_addMonitorHandler = nullptr;
-        std::function<void(Monitor *, IMonitorHandler *)> m_removeMonitorHandler = nullptr;
+        sigslot::signal<Monitor *> m_monitorAdded;
+        sigslot::signal<Monitor *, IMonitorHandler *> m_monitorRemoved;
         std::function<void(const std::string &)> m_questionAsked = nullptr;
         std::function<void(const std::string &)> m_questionAnswered = nullptr;
 };
diff --git a/test/engine/engine_test.cpp b/test/engine/engine_test.cpp
index 006f90d0..ba0f0ab8 100644
--- a/test/engine/engine_test.cpp
+++ b/test/engine/engine_test.cpp
@@ -106,7 +106,7 @@ TEST(EngineTest, Clear)
 
     AddRemoveMonitorMock removeMonitorMock;
     auto handler = std::bind(&AddRemoveMonitorMock::monitorRemoved, &removeMonitorMock, std::placeholders::_1, std::placeholders::_2);
-    engine.setRemoveMonitorHandler(std::function<void(Monitor *, IMonitorHandler *)>(handler));
+    engine.monitorRemoved().connect(handler);
     engine.setMonitors({ monitor1, monitor2, monitor3, monitor4 });
 
     EXPECT_CALL(removeMonitorMock, monitorRemoved(monitor1.get(), &iface1));
@@ -1581,7 +1581,7 @@ TEST(EngineTest, Monitors)
 
     AddRemoveMonitorMock addMonitorMock;
     auto handler = std::bind(&AddRemoveMonitorMock::monitorAdded, &addMonitorMock, std::placeholders::_1);
-    engine.setAddMonitorHandler(std::function<void(Monitor *)>(handler));
+    engine.monitorAdded().connect(handler);
     engine.setMonitors({});
 
     EXPECT_CALL(addMonitorMock, monitorAdded(m1.get()));
@@ -1691,7 +1691,7 @@ TEST(EngineTest, CreateMissingMonitors)
         Engine engine;
         AddRemoveMonitorMock addMonitorMock;
         auto handler = std::bind(&AddRemoveMonitorMock::monitorAdded, &addMonitorMock, std::placeholders::_1);
-        engine.setAddMonitorHandler(std::function<void(Monitor *)>(handler));
+        engine.monitorAdded().connect(handler);
 
         EXPECT_CALL(addMonitorMock, monitorAdded(m1.get()));
         EXPECT_CALL(addMonitorMock, monitorAdded(m2.get()));
diff --git a/test/mocks/enginemock.h b/test/mocks/enginemock.h
index 7ee6c02f..ceaff437 100644
--- a/test/mocks/enginemock.h
+++ b/test/mocks/enginemock.h
@@ -124,8 +124,8 @@ class EngineMock : public IEngine
 
         MOCK_METHOD(const std::vector<std::shared_ptr<Monitor>> &, monitors, (), (const, override));
         MOCK_METHOD(void, setMonitors, (const std::vector<std::shared_ptr<Monitor>> &), (override));
-        MOCK_METHOD(void, setAddMonitorHandler, (const std::function<void(Monitor *)> &), (override));
-        MOCK_METHOD(void, setRemoveMonitorHandler, (const std::function<void(Monitor *, IMonitorHandler *)> &), (override));
+        MOCK_METHOD(sigslot::signal<Monitor *> &, monitorAdded, (), (override));
+        MOCK_METHOD((sigslot::signal<Monitor *, IMonitorHandler *> &), monitorRemoved, (), (override));
 
         MOCK_METHOD(const std::function<void(const std::string &)> &, questionAsked, (), (const, override));
         MOCK_METHOD(void, setQuestionAsked, (const std::function<void(const std::string &)> &), (override));

From 118be3e2a3152b8d679a97b3300199c9fdbc5afa Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Thu, 21 Mar 2024 18:29:25 +0100
Subject: [PATCH 43/77] Replace question asked/answered handlers with signals

---
 include/scratchcpp/iengine.h        | 14 ++++----------
 src/blocks/sensingblocks.cpp        | 10 ++++------
 src/engine/internal/engine.cpp      | 14 ++------------
 src/engine/internal/engine.h        | 11 ++++-------
 test/blocks/sensing_blocks_test.cpp | 30 ++++++++++++-----------------
 test/engine/engine_test.cpp         | 28 ---------------------------
 test/mocks/enginemock.h             |  7 ++-----
 7 files changed, 28 insertions(+), 86 deletions(-)

diff --git a/include/scratchcpp/iengine.h b/include/scratchcpp/iengine.h
index f50d864c..56df1aa4 100644
--- a/include/scratchcpp/iengine.h
+++ b/include/scratchcpp/iengine.h
@@ -352,17 +352,11 @@ class LIBSCRATCHCPP_EXPORT IEngine
         /*! Emits when a monitor is removed. */
         virtual sigslot::signal<Monitor *, IMonitorHandler *> &monitorRemoved() = 0;
 
-        /*! Returns the function which is called when a question is asked, for example using the 'ask and wait' block. */
-        virtual const std::function<void(const std::string &)> &questionAsked() const = 0;
+        /*! Emits when a question is asked, for example using the 'ask and wait' block. */
+        virtual sigslot::signal<const std::string &> &questionAsked() = 0;
 
-        /*! Sets the function which is called when a question is asked, for example using the 'ask and wait' block. */
-        virtual void setQuestionAsked(const std::function<void(const std::string &)> &f) = 0;
-
-        /*! Returns the function which should be called when a question is answered. */
-        virtual const std::function<void(const std::string &)> &questionAnswered() const = 0;
-
-        /*! Sets the function which should be called when a question is answered. */
-        virtual void setQuestionAnswered(const std::function<void(const std::string &)> &f) = 0;
+        /*! Emits when a question is answered. */
+        virtual sigslot::signal<const std::string &> &questionAnswered() = 0;
 
         /*! Returns the list of extension names. */
         virtual const std::vector<std::string> &extensions() const = 0;
diff --git a/src/blocks/sensingblocks.cpp b/src/blocks/sensingblocks.cpp
index 180da8b6..7b91c8b0 100644
--- a/src/blocks/sensingblocks.cpp
+++ b/src/blocks/sensingblocks.cpp
@@ -88,7 +88,7 @@ void SensingBlocks::registerBlocks(IEngine *engine)
     engine->addFieldValue(this, "backdrop name", BackdropName);
 
     // Callbacks
-    engine->setQuestionAnswered(&onAnswer);
+    engine->questionAnswered().connect(&onAnswer);
 }
 
 void SensingBlocks::compileDistanceTo(Compiler *compiler)
@@ -946,7 +946,7 @@ void SensingBlocks::askNextQuestion()
 
     Question *question = m_questionList.front().get();
     Target *target = question->vm->target();
-    auto ask = question->vm->engine()->questionAsked();
+    IEngine *engine = question->vm->engine();
 
     // If the target is visible, emit a blank question and show
     // a bubble unless the target was the stage
@@ -954,10 +954,8 @@ void SensingBlocks::askNextQuestion()
         target->setBubbleType(Target::BubbleType::Say);
         target->setBubbleText(question->question);
 
-        if (ask)
-            ask("");
+        engine->questionAsked()("");
     } else {
-        if (ask)
-            ask(question->question);
+        engine->questionAsked()(question->question);
     }
 }
diff --git a/src/engine/internal/engine.cpp b/src/engine/internal/engine.cpp
index a63cd2b4..3b4d757f 100644
--- a/src/engine/internal/engine.cpp
+++ b/src/engine/internal/engine.cpp
@@ -1239,26 +1239,16 @@ sigslot::signal<Monitor *, IMonitorHandler *> &Engine::monitorRemoved()
     return m_monitorRemoved;
 }
 
-const std::function<void(const std::string &)> &Engine::questionAsked() const
+sigslot::signal<const std::string &> &Engine::questionAsked()
 {
     return m_questionAsked;
 }
 
-void Engine::setQuestionAsked(const std::function<void(const std::string &)> &f)
-{
-    m_questionAsked = f;
-}
-
-const std::function<void(const std::string &)> &Engine::questionAnswered() const
+sigslot::signal<const std::string &> &Engine::questionAnswered()
 {
     return m_questionAnswered;
 }
 
-void Engine::setQuestionAnswered(const std::function<void(const std::string &)> &f)
-{
-    m_questionAnswered = f;
-}
-
 const std::vector<std::string> &Engine::extensions() const
 {
     return m_extensions;
diff --git a/src/engine/internal/engine.h b/src/engine/internal/engine.h
index 79b4fb88..1ac3aef3 100644
--- a/src/engine/internal/engine.h
+++ b/src/engine/internal/engine.h
@@ -146,11 +146,8 @@ class Engine : public IEngine
         sigslot::signal<Monitor *> &monitorAdded() override;
         sigslot::signal<Monitor *, IMonitorHandler *> &monitorRemoved() override;
 
-        const std::function<void(const std::string &)> &questionAsked() const override;
-        void setQuestionAsked(const std::function<void(const std::string &)> &f) override;
-
-        const std::function<void(const std::string &)> &questionAnswered() const override;
-        void setQuestionAnswered(const std::function<void(const std::string &)> &f) override;
+        sigslot::signal<const std::string &> &questionAsked() override;
+        sigslot::signal<const std::string &> &questionAnswered() override;
 
         const std::vector<std::string> &extensions() const override;
         void setExtensions(const std::vector<std::string> &newExtensions) override;
@@ -274,8 +271,8 @@ class Engine : public IEngine
 
         sigslot::signal<Monitor *> m_monitorAdded;
         sigslot::signal<Monitor *, IMonitorHandler *> m_monitorRemoved;
-        std::function<void(const std::string &)> m_questionAsked = nullptr;
-        std::function<void(const std::string &)> m_questionAnswered = nullptr;
+        sigslot::signal<const std::string &> m_questionAsked;
+        sigslot::signal<const std::string &> m_questionAnswered;
 };
 
 } // namespace libscratchcpp
diff --git a/test/blocks/sensing_blocks_test.cpp b/test/blocks/sensing_blocks_test.cpp
index c94efc07..ad13634f 100644
--- a/test/blocks/sensing_blocks_test.cpp
+++ b/test/blocks/sensing_blocks_test.cpp
@@ -102,14 +102,6 @@ struct QuestionSpy
         MOCK_METHOD(void, asked, (const std::string &), ());
 };
 
-template<typename T, typename... U>
-size_t getAddress(std::function<T(U...)> f)
-{
-    typedef T(fnType)(U...);
-    fnType **fnPointer = f.template target<fnType *>();
-    return (size_t)*fnPointer;
-}
-
 TEST_F(SensingBlocksTest, Name)
 {
     ASSERT_EQ(m_section->name(), "Sensing");
@@ -181,13 +173,13 @@ TEST_F(SensingBlocksTest, RegisterBlocks)
     EXPECT_CALL(m_engineMock, addFieldValue(m_section.get(), "backdrop name", SensingBlocks::BackdropName));
 
     // Callbacks
-    std::function<void(const std::string &)> questionAnsweredRef = &SensingBlocks::onAnswer;
-    std::function<void(const std::string &)> questionAnswered;
-    EXPECT_CALL(m_engineMock, setQuestionAnswered(_)).WillOnce(SaveArg<0>(&questionAnswered));
+    sigslot::signal<const std::string &> questionAnswered;
+    EXPECT_CALL(m_engineMock, questionAnswered()).WillOnce(ReturnRef(questionAnswered));
 
     m_section->registerBlocks(&m_engineMock);
-    ASSERT_TRUE(questionAnswered);
-    ASSERT_EQ(getAddress(questionAnsweredRef), getAddress(questionAnswered));
+
+    ASSERT_EQ(questionAnswered.slot_count(), 1);
+    ASSERT_EQ(questionAnswered.disconnect(&SensingBlocks::onAnswer), 1);
 }
 
 TEST_F(SensingBlocksTest, DistanceTo)
@@ -381,14 +373,16 @@ TEST_F(SensingBlocksTest, AskAndWaitAndAnswerImpl)
     sprite.setBubbleType(Target::BubbleType::Think);
     Stage stage;
     QuestionSpy spy;
-    std::function<void(const std::string &)> asked = std::bind(&QuestionSpy::asked, &spy, std::placeholders::_1);
+    auto asked = std::bind(&QuestionSpy::asked, &spy, std::placeholders::_1);
+    sigslot::signal<const std::string &> askedSignal;
+    askedSignal.connect(asked);
 
     VirtualMachine vm1(&sprite, &m_engineMock, nullptr);
     vm1.setFunctions(functions);
     vm1.setConstValues(constValues);
 
     // Ask 3 questions (2 where the sprite is visible and 1 where it's invisible)
-    EXPECT_CALL(m_engineMock, questionAsked()).WillOnce(ReturnRef(asked));
+    EXPECT_CALL(m_engineMock, questionAsked()).WillOnce(ReturnRef(askedSignal));
     EXPECT_CALL(spy, asked(""));
     sprite.setVisible(true);
     vm1.setBytecode(bytecode1);
@@ -435,7 +429,7 @@ TEST_F(SensingBlocksTest, AskAndWaitAndAnswerImpl)
     ASSERT_EQ(sprite.bubbleText(), "test1");
 
     // Answer the questions
-    EXPECT_CALL(m_engineMock, questionAsked()).WillOnce(ReturnRef(asked));
+    EXPECT_CALL(m_engineMock, questionAsked()).WillOnce(ReturnRef(askedSignal));
     EXPECT_CALL(spy, asked(""));
     SensingBlocks::onAnswer("hi");
     ASSERT_EQ(sprite.bubbleType(), Target::BubbleType::Say);
@@ -447,7 +441,7 @@ TEST_F(SensingBlocksTest, AskAndWaitAndAnswerImpl)
     ASSERT_EQ(vm1.registerCount(), 1);
     ASSERT_EQ(vm1.getInput(0, 1)->toString(), "hi");
 
-    EXPECT_CALL(m_engineMock, questionAsked()).WillOnce(ReturnRef(asked));
+    EXPECT_CALL(m_engineMock, questionAsked()).WillOnce(ReturnRef(askedSignal));
     EXPECT_CALL(spy, asked("test3"));
     SensingBlocks::onAnswer("hello");
     ASSERT_TRUE(sprite.bubbleText().empty());
@@ -457,7 +451,7 @@ TEST_F(SensingBlocksTest, AskAndWaitAndAnswerImpl)
     ASSERT_EQ(vm1.registerCount(), 1);
     ASSERT_EQ(vm1.getInput(0, 1)->toString(), "hello");
 
-    EXPECT_CALL(m_engineMock, questionAsked()).WillOnce(ReturnRef(asked));
+    EXPECT_CALL(m_engineMock, questionAsked()).WillOnce(ReturnRef(askedSignal));
     EXPECT_CALL(spy, asked("test2"));
     SensingBlocks::onAnswer("world");
     ASSERT_TRUE(sprite.bubbleText().empty());
diff --git a/test/engine/engine_test.cpp b/test/engine/engine_test.cpp
index ba0f0ab8..d76eb00b 100644
--- a/test/engine/engine_test.cpp
+++ b/test/engine/engine_test.cpp
@@ -54,14 +54,6 @@ class AddRemoveMonitorMock
         MOCK_METHOD(void, monitorRemoved, (Monitor *, IMonitorHandler *));
 };
 
-template<typename T, typename... U>
-size_t getAddress(std::function<T(U...)> f)
-{
-    typedef T(fnType)(U...);
-    fnType **fnPointer = f.template target<fnType *>();
-    return (size_t)*fnPointer;
-}
-
 TEST(EngineTest, Clock)
 {
     Engine engine;
@@ -1715,26 +1707,6 @@ void questionFunction(const std::string &)
 {
 }
 
-TEST(EngineTest, QuestionAsked)
-{
-    Engine engine;
-    ASSERT_EQ(engine.questionAsked(), nullptr);
-
-    static const std::function<void(const std::string &)> f = &questionFunction;
-    engine.setQuestionAsked(&questionFunction);
-    ASSERT_EQ(getAddress(engine.questionAsked()), getAddress(f));
-}
-
-TEST(EngineTest, QuestionAnswered)
-{
-    Engine engine;
-    ASSERT_EQ(engine.questionAnswered(), nullptr);
-
-    static const std::function<void(const std::string &)> f = &questionFunction;
-    engine.setQuestionAnswered(f);
-    ASSERT_EQ(getAddress(engine.questionAnswered()), getAddress(f));
-}
-
 TEST(EngineTest, Clones)
 {
     Project p("clones.sb3");
diff --git a/test/mocks/enginemock.h b/test/mocks/enginemock.h
index ceaff437..ebda814a 100644
--- a/test/mocks/enginemock.h
+++ b/test/mocks/enginemock.h
@@ -127,11 +127,8 @@ class EngineMock : public IEngine
         MOCK_METHOD(sigslot::signal<Monitor *> &, monitorAdded, (), (override));
         MOCK_METHOD((sigslot::signal<Monitor *, IMonitorHandler *> &), monitorRemoved, (), (override));
 
-        MOCK_METHOD(const std::function<void(const std::string &)> &, questionAsked, (), (const, override));
-        MOCK_METHOD(void, setQuestionAsked, (const std::function<void(const std::string &)> &), (override));
-
-        MOCK_METHOD(const std::function<void(const std::string &)> &, questionAnswered, (), (const, override));
-        MOCK_METHOD(void, setQuestionAnswered, (const std::function<void(const std::string &)> &), (override));
+        MOCK_METHOD(sigslot::signal<const std::string &> &, questionAsked, (), (override));
+        MOCK_METHOD(sigslot::signal<const std::string &> &, questionAnswered, (), (override));
 
         MOCK_METHOD(std::vector<std::string> &, extensions, (), (const, override));
         MOCK_METHOD(void, setExtensions, (const std::vector<std::string> &), (override));

From d3e1e251d25f7d48f1a805a9e28618a62a070d5f Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Fri, 22 Mar 2024 15:53:43 +0100
Subject: [PATCH 44/77] Replace download progress callback with a signal

---
 include/scratchcpp/project.h           |  3 ++-
 src/internal/iprojectdownloader.h      |  3 ++-
 src/internal/projectdownloader.cpp     | 14 +++-----------
 src/internal/projectdownloader.h       |  5 ++---
 src/internal/projectdownloaderstub.cpp |  3 ++-
 src/internal/projectdownloaderstub.h   |  5 ++++-
 src/project.cpp                        |  6 +++---
 src/project_p.cpp                      |  4 ++--
 src/project_p.h                        |  2 +-
 test/mocks/projectdownloadermock.h     |  2 +-
 test/project/project_test.cpp          | 10 +++++-----
 11 files changed, 27 insertions(+), 30 deletions(-)

diff --git a/include/scratchcpp/project.h b/include/scratchcpp/project.h
index 39ed3996..414ae47b 100644
--- a/include/scratchcpp/project.h
+++ b/include/scratchcpp/project.h
@@ -7,6 +7,7 @@
 #include "spimpl.h"
 
 #include "global.h"
+#include "signal.h"
 
 namespace libscratchcpp
 {
@@ -42,7 +43,7 @@ class LIBSCRATCHCPP_EXPORT Project
 
         std::shared_ptr<IEngine> engine() const;
 
-        void setDownloadProgressCallback(const std::function<void(unsigned int, unsigned int)> &&f);
+        sigslot::signal<unsigned int, unsigned int> &downloadProgressChanged();
 
     private:
         spimpl::unique_impl_ptr<ProjectPrivate> impl;
diff --git a/src/internal/iprojectdownloader.h b/src/internal/iprojectdownloader.h
index 84fedbfd..bafc4562 100644
--- a/src/internal/iprojectdownloader.h
+++ b/src/internal/iprojectdownloader.h
@@ -5,6 +5,7 @@
 #include <string>
 #include <vector>
 #include <functional>
+#include <scratchcpp/signal.h>
 
 namespace libscratchcpp
 {
@@ -18,7 +19,7 @@ class IProjectDownloader
         virtual bool downloadAssets(const std::vector<std::string> &assetIds) = 0;
         virtual void cancel() = 0;
 
-        virtual void setDownloadProgressCallback(const std::function<void(unsigned int, unsigned int)> &f) = 0;
+        virtual sigslot::signal<unsigned int, unsigned int> &downloadProgressChanged() = 0;
 
         virtual const std::string &json() const = 0;
         virtual const std::vector<std::string> &assets() const = 0;
diff --git a/src/internal/projectdownloader.cpp b/src/internal/projectdownloader.cpp
index 92ed4dff..15e31246 100644
--- a/src/internal/projectdownloader.cpp
+++ b/src/internal/projectdownloader.cpp
@@ -149,13 +149,7 @@ bool ProjectDownloader::downloadAssets(const std::vector<std::string> &assetIds)
                 m_assets[index] = downloader->text();
                 m_downloadedAssetCount++;
                 std::cout << "Downloaded assets: " << m_downloadedAssetCount << " of " << count << std::endl;
-
-                m_downloadProgressCallbackMutex.lock();
-
-                if (m_downloadProgressCallback)
-                    m_downloadProgressCallback(m_downloadedAssetCount, count);
-
-                m_downloadProgressCallbackMutex.unlock();
+                m_downloadProgressChanged(m_downloadedAssetCount, count);
                 m_assetsMutex.unlock();
             }
         }
@@ -182,11 +176,9 @@ void ProjectDownloader::cancel()
     m_downloadedAssetCount = 0;
 }
 
-void ProjectDownloader::setDownloadProgressCallback(const std::function<void(unsigned int, unsigned int)> &f)
+sigslot::signal<unsigned int, unsigned int> &ProjectDownloader::downloadProgressChanged()
 {
-    m_downloadProgressCallbackMutex.lock();
-    m_downloadProgressCallback = f;
-    m_downloadProgressCallbackMutex.unlock();
+    return m_downloadProgressChanged;
 }
 
 const std::string &ProjectDownloader::json() const
diff --git a/src/internal/projectdownloader.h b/src/internal/projectdownloader.h
index 2d1c111f..15b47472 100644
--- a/src/internal/projectdownloader.h
+++ b/src/internal/projectdownloader.h
@@ -23,7 +23,7 @@ class ProjectDownloader : public IProjectDownloader
         bool downloadAssets(const std::vector<std::string> &assetIds) override;
         void cancel() override;
 
-        void setDownloadProgressCallback(const std::function<void(unsigned int, unsigned int)> &f) override;
+        sigslot::signal<unsigned int, unsigned int> &downloadProgressChanged() override;
 
         const std::string &json() const override;
         const std::vector<std::string> &assets() const override;
@@ -38,8 +38,7 @@ class ProjectDownloader : public IProjectDownloader
         std::atomic<unsigned int> m_downloadedAssetCount = 0;
         bool m_cancel = false;
         std::mutex m_cancelMutex;
-        std::function<void(unsigned int, unsigned int)> m_downloadProgressCallback = nullptr;
-        std::mutex m_downloadProgressCallbackMutex;
+        sigslot::signal<unsigned int, unsigned int> m_downloadProgressChanged;
 };
 
 } // namespace libscratchcpp
diff --git a/src/internal/projectdownloaderstub.cpp b/src/internal/projectdownloaderstub.cpp
index 67026fa3..a0d503b8 100644
--- a/src/internal/projectdownloaderstub.cpp
+++ b/src/internal/projectdownloaderstub.cpp
@@ -22,8 +22,9 @@ void ProjectDownloaderStub::cancel()
 {
 }
 
-void ProjectDownloaderStub::setDownloadProgressCallback(const std::function<void(unsigned int, unsigned int)> &)
+sigslot::signal<unsigned int, unsigned int> &ProjectDownloaderStub::downloadProgressChanged()
 {
+    return m_downloadProgressChanged;
 }
 
 const std::string &ProjectDownloaderStub::json() const
diff --git a/src/internal/projectdownloaderstub.h b/src/internal/projectdownloaderstub.h
index 32660b1f..b02e80db 100644
--- a/src/internal/projectdownloaderstub.h
+++ b/src/internal/projectdownloaderstub.h
@@ -16,11 +16,14 @@ class ProjectDownloaderStub : public IProjectDownloader
         bool downloadAssets(const std::vector<std::string> &) override;
         void cancel() override;
 
-        virtual void setDownloadProgressCallback(const std::function<void(unsigned int, unsigned int)> &) override;
+        sigslot::signal<unsigned int, unsigned int> &downloadProgressChanged() override;
 
         const std::string &json() const override;
         const std::vector<std::string> &assets() const override;
         unsigned int downloadedAssetCount() const override;
+
+    private:
+        sigslot::signal<unsigned int, unsigned int> m_downloadProgressChanged;
 };
 
 } // namespace libscratchcpp
diff --git a/src/project.cpp b/src/project.cpp
index a489c842..b5d94afe 100644
--- a/src/project.cpp
+++ b/src/project.cpp
@@ -100,10 +100,10 @@ std::shared_ptr<IEngine> Project::engine() const
 }
 
 /*!
- * Sets the function which will be called when the asset download progress changes.
+ * Emits when the asset download progress changes.
  * \note The first parameter is the number of downloaded assets and the latter is the number of all assets to download.
  */
-void Project::setDownloadProgressCallback(const std::function<void(unsigned int, unsigned int)> &&f)
+sigslot::signal<unsigned int, unsigned int> &Project::downloadProgressChanged()
 {
-    impl->setDownloadProgressCallback(f);
+    return impl->downloadProgressChanged();
 }
diff --git a/src/project_p.cpp b/src/project_p.cpp
index f581f117..951c3f3f 100644
--- a/src/project_p.cpp
+++ b/src/project_p.cpp
@@ -183,7 +183,7 @@ void ProjectPrivate::setScratchVersion(ScratchVersion version)
         std::cerr << "Unsupported Scratch version: " << static_cast<int>(version) << std::endl;
 }
 
-void ProjectPrivate::setDownloadProgressCallback(const std::function<void(unsigned int, unsigned int)> &f)
+sigslot::signal<unsigned int, unsigned int> &ProjectPrivate::downloadProgressChanged()
 {
-    downloader->setDownloadProgressCallback(f);
+    return downloader->downloadProgressChanged();
 }
diff --git a/src/project_p.h b/src/project_p.h
index 8eddebd0..6ccabfff 100644
--- a/src/project_p.h
+++ b/src/project_p.h
@@ -28,7 +28,7 @@ struct ProjectPrivate
         void detectScratchVersion();
         void setScratchVersion(ScratchVersion version);
 
-        void setDownloadProgressCallback(const std::function<void(unsigned int, unsigned int)> &f);
+        sigslot::signal<unsigned int, unsigned int> &downloadProgressChanged();
 
         ScratchVersion scratchVersion = ScratchVersion::Invalid;
         std::string fileName;
diff --git a/test/mocks/projectdownloadermock.h b/test/mocks/projectdownloadermock.h
index 349c55ea..d9897b8f 100644
--- a/test/mocks/projectdownloadermock.h
+++ b/test/mocks/projectdownloadermock.h
@@ -12,7 +12,7 @@ class ProjectDownloaderMock : public IProjectDownloader
         MOCK_METHOD(bool, downloadAssets, (const std::vector<std::string> &), (override));
         MOCK_METHOD(void, cancel, (), (override));
 
-        MOCK_METHOD(void, setDownloadProgressCallback, (const std::function<void(unsigned int, unsigned int)> &), (override));
+        MOCK_METHOD((sigslot::signal<unsigned int, unsigned int> &), downloadProgressChanged, (), (override));
 
         MOCK_METHOD(const std::string &, json, (), (const, override));
         MOCK_METHOD(const std::vector<std::string> &, assets, (), (const, override));
diff --git a/test/project/project_test.cpp b/test/project/project_test.cpp
index 8d99bca5..c4346a22 100644
--- a/test/project/project_test.cpp
+++ b/test/project/project_test.cpp
@@ -9,6 +9,7 @@
 using namespace libscratchcpp;
 
 using ::testing::Return;
+using ::testing::ReturnRef;
 
 class ProjectTest : public testing::Test
 {
@@ -158,7 +159,7 @@ TEST_F(ProjectTest, ScratchVersion)
     ASSERT_EQ(p.scratchVersion(), ScratchVersion::Scratch3);
 }
 
-TEST(LoadProjectTest, DownloadProgressCallback)
+TEST(LoadProjectTest, DownloadProgressChanged)
 {
     ProjectDownloaderFactoryMock factory;
     auto downloader = std::make_shared<ProjectDownloaderMock>();
@@ -168,8 +169,7 @@ TEST(LoadProjectTest, DownloadProgressCallback)
     ProjectPrivate p;
     ProjectPrivate::downloaderFactory = nullptr;
 
-    auto lambda = [](unsigned int, unsigned int) {};
-    // TODO: Check the function parameter, if possible (std::function doesn't have operator== for this)
-    EXPECT_CALL(*downloader, setDownloadProgressCallback);
-    p.setDownloadProgressCallback(lambda);
+    sigslot::signal<unsigned int, unsigned int> signal;
+    EXPECT_CALL(*downloader, downloadProgressChanged).WillOnce(ReturnRef(signal));
+    ASSERT_EQ(&p.downloadProgressChanged(), &signal);
 }

From 5d0ac931b49b122b8c6b3ca52f0801925aa064f3 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Fri, 22 Mar 2024 16:43:32 +0100
Subject: [PATCH 45/77] Add stop all bypass test

---
 test/engine/engine_test.cpp |  20 ++++++++++++++++++++
 test/stop_all_bypass.sb3    | Bin 0 -> 1608 bytes
 2 files changed, 20 insertions(+)
 create mode 100644 test/stop_all_bypass.sb3

diff --git a/test/engine/engine_test.cpp b/test/engine/engine_test.cpp
index d76eb00b..49816ab3 100644
--- a/test/engine/engine_test.cpp
+++ b/test/engine/engine_test.cpp
@@ -1876,6 +1876,26 @@ TEST(EngineTest, StopAll)
     ASSERT_FALSE(engine->isRunning());
 }
 
+TEST(EngineTest, StopAllBypass)
+{
+    Project p("stop_all_bypass.sb3");
+    ASSERT_TRUE(p.load());
+    p.run();
+
+    auto engine = p.engine();
+
+    Stage *stage = engine->stage();
+    ASSERT_TRUE(stage);
+
+    ASSERT_VAR(stage, "i");
+    ASSERT_EQ(GET_VAR(stage, "i")->value().toInt(), 1);
+
+    ASSERT_VAR(stage, "j");
+    ASSERT_EQ(GET_VAR(stage, "j")->value().toInt(), 5);
+
+    ASSERT_FALSE(engine->isRunning());
+}
+
 TEST(EngineTest, StopOtherScriptsInSprite)
 {
     Project p("stop_other_scripts_in_sprite.sb3");
diff --git a/test/stop_all_bypass.sb3 b/test/stop_all_bypass.sb3
new file mode 100644
index 0000000000000000000000000000000000000000..b2c826897e2f2c5466081224c97e4b9c621388e8
GIT binary patch

From 85f8b134c250faffe95d6b67680a50bb1ed8e390 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Fri, 22 Mar 2024 23:06:37 +0100
Subject: [PATCH 46/77] Add thread about to stop signal

---
 include/scratchcpp/iengine.h   |   3 ++
 src/engine/internal/engine.cpp |  30 ++++++++++++-
 src/engine/internal/engine.h   |   2 +
 test/3_threads.sb3             | Bin 0 -> 908 bytes
 test/engine/engine_test.cpp    |  74 +++++++++++++++++++++++++++++++--
 test/mocks/enginemock.h        |   1 +
 6 files changed, 106 insertions(+), 4 deletions(-)
 create mode 100644 test/3_threads.sb3

diff --git a/include/scratchcpp/iengine.h b/include/scratchcpp/iengine.h
index 56df1aa4..88e95521 100644
--- a/include/scratchcpp/iengine.h
+++ b/include/scratchcpp/iengine.h
@@ -113,6 +113,9 @@ class LIBSCRATCHCPP_EXPORT IEngine
         /*! Emits when rendering should occur. */
         virtual sigslot::signal<> &aboutToRender() = 0;
 
+        /*! Emits when a script is about to stop. */
+        virtual sigslot::signal<VirtualMachine *> &threadAboutToStop() = 0;
+
         /*! Returns true if the project is currently running. */
         virtual bool isRunning() const = 0;
 
diff --git a/src/engine/internal/engine.cpp b/src/engine/internal/engine.cpp
index 3b4d757f..4665d438 100644
--- a/src/engine/internal/engine.cpp
+++ b/src/engine/internal/engine.cpp
@@ -63,6 +63,11 @@ void Engine::clear()
     for (auto monitor : m_monitors)
         m_monitorRemoved(monitor.get(), monitor->impl->iface);
 
+    for (auto thread : m_threads) {
+        if (!thread->atEnd())
+            m_threadAboutToStop(thread.get());
+    }
+
     m_sections.clear();
     m_targets.clear();
     m_broadcasts.clear();
@@ -328,6 +333,11 @@ void Engine::stop()
     } else {
         // If there isn't any active thread, it means the project was stopped from the outside
         // In this case all threads should be removed and the project should be considered stopped
+        for (auto thread : m_threads) {
+            if (!thread->atEnd())
+                m_threadAboutToStop(thread.get());
+        }
+
         m_threads.clear();
         m_running = false;
     }
@@ -511,6 +521,11 @@ sigslot::signal<> &Engine::aboutToRender()
     return m_aboutToRedraw;
 }
 
+sigslot::signal<VirtualMachine *> &Engine::threadAboutToStop()
+{
+    return m_threadAboutToStop;
+}
+
 std::vector<std::shared_ptr<VirtualMachine>> Engine::stepThreads()
 {
     // https://github.com/scratchfoundation/scratch-vm/blob/develop/src/engine/sequencer.js#L70-L173
@@ -548,8 +563,14 @@ std::vector<std::shared_ptr<VirtualMachine>> Engine::stepThreads()
         }
 
         // Remove threads in m_threadsToStop
-        for (auto thread : m_threadsToStop)
+        for (auto thread : m_threadsToStop) {
+            if (!thread->atEnd())
+                m_threadAboutToStop(thread.get());
+
             m_threads.erase(std::remove(m_threads.begin(), m_threads.end(), thread), m_threads.end());
+        }
+
+        m_threadsToStop.clear();
 
         // Remove inactive threads (and add them to doneThreads)
         m_threads.erase(
@@ -1658,6 +1679,10 @@ void Engine::stopThread(VirtualMachine *thread)
 {
     // https://github.com/scratchfoundation/scratch-vm/blob/f1aa92fad79af17d9dd1c41eeeadca099339a9f1/src/engine/runtime.js#L1667-L1672
     assert(thread);
+
+    if (!thread->atEnd())
+        m_threadAboutToStop(thread);
+
     thread->kill();
 }
 
@@ -1668,6 +1693,9 @@ std::shared_ptr<VirtualMachine> Engine::restartThread(std::shared_ptr<VirtualMac
     auto it = std::find(m_threads.begin(), m_threads.end(), thread);
 
     if (it != m_threads.end()) {
+        if (!thread->atEnd())
+            m_threadAboutToStop(thread.get());
+
         auto i = it - m_threads.begin();
         m_threads[i] = newThread;
         return newThread;
diff --git a/src/engine/internal/engine.h b/src/engine/internal/engine.h
index 1ac3aef3..69b05dd6 100644
--- a/src/engine/internal/engine.h
+++ b/src/engine/internal/engine.h
@@ -50,6 +50,7 @@ class Engine : public IEngine
         void stopEventLoop() override;
 
         sigslot::signal<> &aboutToRender() override;
+        sigslot::signal<VirtualMachine *> &threadAboutToStop() override;
 
         bool isRunning() const override;
 
@@ -266,6 +267,7 @@ class Engine : public IEngine
         bool m_running = false;
         bool m_redrawRequested = false;
         sigslot::signal<> m_aboutToRedraw;
+        sigslot::signal<VirtualMachine *> m_threadAboutToStop;
         bool m_stopEventLoop = false;
         std::mutex m_stopEventLoopMutex;
 
diff --git a/test/3_threads.sb3 b/test/3_threads.sb3
new file mode 100644
index 0000000000000000000000000000000000000000..10caf280b96ad18151ead2015bd848025b60b988
GIT binary patch
diff --git a/test/engine/engine_test.cpp b/test/engine/engine_test.cpp
index 49816ab3..2b0064fe 100644
--- a/test/engine/engine_test.cpp
+++ b/test/engine/engine_test.cpp
@@ -47,6 +47,12 @@ class RedrawMock
         MOCK_METHOD(void, redraw, ());
 };
 
+class ThreadAboutToStopMock
+{
+    public:
+        MOCK_METHOD(void, threadRemoved, (VirtualMachine *));
+};
+
 class AddRemoveMonitorMock
 {
     public:
@@ -109,6 +115,44 @@ TEST(EngineTest, Clear)
     ASSERT_TRUE(engine.monitors().empty());
 }
 
+TEST(EngineTest, ClearThreadAboutToStopSignal)
+{
+    Project p("3_threads.sb3");
+    ASSERT_TRUE(p.load());
+    auto engine = p.engine();
+
+    engine->start();
+    engine->step();
+
+    ThreadAboutToStopMock threadRemovedMock;
+    EXPECT_CALL(threadRemovedMock, threadRemoved(_)).Times(3).WillRepeatedly(WithArgs<0>(Invoke([](VirtualMachine *vm) {
+        ASSERT_TRUE(vm);
+        ASSERT_FALSE(vm->atEnd());
+    })));
+
+    engine->threadAboutToStop().connect(&ThreadAboutToStopMock::threadRemoved, &threadRemovedMock);
+    engine->clear();
+}
+
+TEST(EngineTest, StopThreadAboutToStopSignal)
+{
+    Project p("3_threads.sb3");
+    ASSERT_TRUE(p.load());
+    auto engine = p.engine();
+
+    engine->start();
+    engine->step();
+
+    ThreadAboutToStopMock threadRemovedMock;
+    EXPECT_CALL(threadRemovedMock, threadRemoved(_)).Times(3).WillRepeatedly(WithArgs<0>(Invoke([](VirtualMachine *vm) {
+        ASSERT_TRUE(vm);
+        ASSERT_FALSE(vm->atEnd());
+    })));
+
+    engine->threadAboutToStop().connect(&ThreadAboutToStopMock::threadRemoved, &threadRemovedMock);
+    engine->stop();
+}
+
 TEST(EngineTest, CompileAndExecuteMonitors)
 {
     Engine engine;
@@ -1840,10 +1884,18 @@ TEST(EngineTest, BroadcastsProject)
 {
     Project p("broadcasts.sb3");
     ASSERT_TRUE(p.load());
-    p.run();
 
     auto engine = p.engine();
+
+    ThreadAboutToStopMock threadRemovedMock;
+    EXPECT_CALL(threadRemovedMock, threadRemoved(_)).Times(21).WillRepeatedly(WithArgs<0>(Invoke([](VirtualMachine *vm) {
+        ASSERT_TRUE(vm);
+        ASSERT_FALSE(vm->atEnd());
+    })));
+
+    engine->threadAboutToStop().connect(&ThreadAboutToStopMock::threadRemoved, &threadRemovedMock);
     engine->setFps(1000);
+    p.run();
 
     Stage *stage = engine->stage();
     ASSERT_TRUE(stage);
@@ -1880,10 +1932,18 @@ TEST(EngineTest, StopAllBypass)
 {
     Project p("stop_all_bypass.sb3");
     ASSERT_TRUE(p.load());
-    p.run();
 
     auto engine = p.engine();
 
+    ThreadAboutToStopMock threadRemovedMock;
+    EXPECT_CALL(threadRemovedMock, threadRemoved(_)).Times(2).WillRepeatedly(WithArgs<0>(Invoke([](VirtualMachine *vm) {
+        ASSERT_TRUE(vm);
+        ASSERT_FALSE(vm->atEnd());
+    })));
+
+    engine->threadAboutToStop().connect(&ThreadAboutToStopMock::threadRemoved, &threadRemovedMock);
+    p.run();
+
     Stage *stage = engine->stage();
     ASSERT_TRUE(stage);
 
@@ -1900,10 +1960,18 @@ TEST(EngineTest, StopOtherScriptsInSprite)
 {
     Project p("stop_other_scripts_in_sprite.sb3");
     ASSERT_TRUE(p.load());
-    p.run();
 
     auto engine = p.engine();
 
+    ThreadAboutToStopMock threadRemovedMock;
+    EXPECT_CALL(threadRemovedMock, threadRemoved(_)).Times(4).WillRepeatedly(WithArgs<0>(Invoke([](VirtualMachine *vm) {
+        ASSERT_TRUE(vm);
+        ASSERT_FALSE(vm->atEnd());
+    })));
+
+    engine->threadAboutToStop().connect(&ThreadAboutToStopMock::threadRemoved, &threadRemovedMock);
+    p.run();
+
     Stage *stage = engine->stage();
     ASSERT_TRUE(stage);
 
diff --git a/test/mocks/enginemock.h b/test/mocks/enginemock.h
index ebda814a..1a3d67ee 100644
--- a/test/mocks/enginemock.h
+++ b/test/mocks/enginemock.h
@@ -33,6 +33,7 @@ class EngineMock : public IEngine
         MOCK_METHOD(void, stopEventLoop, (), (override));
 
         MOCK_METHOD(sigslot::signal<> &, aboutToRender, (), (override));
+        MOCK_METHOD(sigslot::signal<VirtualMachine *> &, threadAboutToStop, (), (override));
 
         MOCK_METHOD(bool, isRunning, (), (const, override));
 

From 049fc346557cc6c0cd2bf9df3788d58f32b64c59 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Fri, 22 Mar 2024 23:31:24 +0100
Subject: [PATCH 47/77] Remove stopped threads in blocks implementation

---
 src/blocks/looksblocks.cpp  | 14 ++++++++++++++
 src/blocks/looksblocks.h    |  1 +
 src/blocks/motionblocks.cpp |  8 ++++++++
 src/blocks/motionblocks.h   |  1 +
 src/blocks/soundblocks.cpp  | 10 +++++++++-
 5 files changed, 33 insertions(+), 1 deletion(-)

diff --git a/src/blocks/looksblocks.cpp b/src/blocks/looksblocks.cpp
index 0072ed45..cc1eb369 100644
--- a/src/blocks/looksblocks.cpp
+++ b/src/blocks/looksblocks.cpp
@@ -85,6 +85,20 @@ void LooksBlocks::registerBlocks(IEngine *engine)
     engine->addFieldValue(this, "backward", Backward);
 }
 
+void LooksBlocks::onInit(IEngine *engine)
+{
+    engine->threadAboutToStop().connect([](VirtualMachine *vm) {
+        m_timeMap.erase(vm);
+
+        for (auto it = m_waitingBubbles.begin(); it != m_waitingBubbles.end();) {
+            if (it->second == vm)
+                m_waitingBubbles.erase(it);
+            else
+                it++;
+        }
+    });
+}
+
 void LooksBlocks::compileSayForSecs(Compiler *compiler)
 {
     compiler->addInput(MESSAGE);
diff --git a/src/blocks/looksblocks.h b/src/blocks/looksblocks.h
index 91a598b4..60d54fc5 100644
--- a/src/blocks/looksblocks.h
+++ b/src/blocks/looksblocks.h
@@ -61,6 +61,7 @@ class LooksBlocks : public IBlockSection
         std::string name() const override;
 
         void registerBlocks(IEngine *engine) override;
+        void onInit(IEngine *engine) override;
 
         static void compileSayForSecs(Compiler *compiler);
         static void compileSay(Compiler *compiler);
diff --git a/src/blocks/motionblocks.cpp b/src/blocks/motionblocks.cpp
index c49a3dd0..012d436d 100644
--- a/src/blocks/motionblocks.cpp
+++ b/src/blocks/motionblocks.cpp
@@ -71,6 +71,14 @@ void MotionBlocks::registerBlocks(IEngine *engine)
     engine->addFieldValue(this, "all around", AllAround);
 }
 
+void MotionBlocks::onInit(IEngine *engine)
+{
+    engine->threadAboutToStop().connect([](VirtualMachine *vm) {
+        m_timeMap.erase(vm);
+        m_glideMap.erase(vm);
+    });
+}
+
 void MotionBlocks::compileMoveSteps(Compiler *compiler)
 {
     compiler->addInput(STEPS);
diff --git a/src/blocks/motionblocks.h b/src/blocks/motionblocks.h
index 6c3e4cac..0b62ee59 100644
--- a/src/blocks/motionblocks.h
+++ b/src/blocks/motionblocks.h
@@ -47,6 +47,7 @@ class MotionBlocks : public IBlockSection
         std::string name() const override;
 
         void registerBlocks(IEngine *engine) override;
+        void onInit(IEngine *engine) override;
 
         static void compileMoveSteps(Compiler *compiler);
         static void compileTurnRight(Compiler *compiler);
diff --git a/src/blocks/soundblocks.cpp b/src/blocks/soundblocks.cpp
index 4543f6c3..fab7302e 100644
--- a/src/blocks/soundblocks.cpp
+++ b/src/blocks/soundblocks.cpp
@@ -36,7 +36,15 @@ void SoundBlocks::registerBlocks(IEngine *engine)
 void SoundBlocks::onInit(IEngine *engine)
 {
     m_waitingSounds.clear();
-    // TODO: Remove stopped threads from m_waitingSounds
+
+    engine->threadAboutToStop().connect([](VirtualMachine *vm) {
+        for (auto it = m_waitingSounds.begin(); it != m_waitingSounds.end();) {
+            if (it->second == vm)
+                m_waitingSounds.erase(it);
+            else
+                it++;
+        }
+    });
 }
 
 bool SoundBlocks::compilePlayCommon(Compiler *compiler, bool untilDone, bool *byIndex)

From fabdcadf8dd05b1a52ee6c100329af16eba8d81a Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Sat, 23 Mar 2024 14:34:22 +0100
Subject: [PATCH 48/77] fix #444: Fix precision when converting double to
 string

---
 include/scratchcpp/value.h          | 34 ++++++++++++++++++--
 test/scratch_classes/value_test.cpp | 49 +++++++++++++++++++++++++----
 2 files changed, 74 insertions(+), 9 deletions(-)

diff --git a/include/scratchcpp/value.h b/include/scratchcpp/value.h
index 7f872213..8f52a872 100644
--- a/include/scratchcpp/value.h
+++ b/include/scratchcpp/value.h
@@ -7,6 +7,7 @@
 #include <limits>
 #include <ctgmath>
 #include <cassert>
+#include <iomanip>
 #include <utf8.h>
 
 #include "global.h"
@@ -40,7 +41,7 @@ class LIBSCRATCHCPP_EXPORT Value
         Value(float numberValue) :
             m_type(Type::Double)
         {
-            m_doubleValue = numberValue;
+            m_doubleValue = floatToDouble(numberValue);
         }
 
         /*! Constructs a number Value. */
@@ -376,7 +377,7 @@ class LIBSCRATCHCPP_EXPORT Value
                 m_stringValue.~basic_string();
 
             m_type = Type::Double;
-            m_doubleValue = v;
+            m_doubleValue = floatToDouble(v);
             return *this;
         }
 
@@ -943,7 +944,7 @@ class LIBSCRATCHCPP_EXPORT Value
         static std::string doubleToString(double v)
         {
             std::stringstream stream;
-            stream << v;
+            stream << std::setprecision(std::max(16u, digitCount(v))) << v;
             std::string s = stream.str();
             std::size_t index;
 
@@ -961,6 +962,33 @@ class LIBSCRATCHCPP_EXPORT Value
 
             return s;
         }
+
+        static double floatToDouble(float v)
+        {
+            unsigned int digits = digitCount(v);
+            double f = std::pow(10, digits);
+            return std::round(v * f) / f;
+        }
+
+        template<typename T>
+        static unsigned int digitCount(T v)
+        {
+            const T epsilon = 0.0000001;
+            T intpart;
+            unsigned int i = 1, j = 0;
+
+            if (std::abs(std::modf(v, &intpart)) >= epsilon) {
+                T tmp_intpart;
+
+                while (std::abs(std::modf(v * std::pow(10, i), &tmp_intpart)) >= epsilon)
+                    i++;
+            }
+
+            while (std::abs(intpart / pow(10, j)) >= 1.0)
+                j++;
+
+            return i + j;
+        }
 };
 
 } // namespace libscratchcpp
diff --git a/test/scratch_classes/value_test.cpp b/test/scratch_classes/value_test.cpp
index de214c86..31c91bdb 100644
--- a/test/scratch_classes/value_test.cpp
+++ b/test/scratch_classes/value_test.cpp
@@ -20,7 +20,7 @@ TEST(ValueTest, DefaultConstructor)
 TEST(ValueTest, FloatConstructor)
 {
     Value v(3.14f);
-    ASSERT_EQ(v.toDouble(), 3.14f);
+    ASSERT_EQ(v.toDouble(), 3.14);
     ASSERT_EQ(v.type(), Value::Type::Double);
     ASSERT_FALSE(v.isInfinity());
     ASSERT_FALSE(v.isNegativeInfinity());
@@ -385,7 +385,7 @@ TEST(ValueTest, FloatAssignment)
 {
     Value v;
     v = 3.14f;
-    ASSERT_EQ(v.toDouble(), 3.14f);
+    ASSERT_EQ(v.toDouble(), 3.14);
     ASSERT_EQ(v.type(), Value::Type::Double);
     ASSERT_FALSE(v.isInfinity());
     ASSERT_FALSE(v.isNegativeInfinity());
@@ -930,12 +930,12 @@ TEST(ValueTest, ToDouble)
     v = 2.54;
     ASSERT_EQ(v.toDouble(), 2.54);
     v = 2.54f;
-    ASSERT_EQ(v.toDouble(), 2.54f);
+    ASSERT_EQ(v.toDouble(), 2.54);
 
     v = -2.54;
     ASSERT_EQ(v.toDouble(), -2.54);
     v = -2.54f;
-    ASSERT_EQ(v.toDouble(), -2.54f);
+    ASSERT_EQ(v.toDouble(), -2.54);
 
     v = false;
     ASSERT_EQ(v.toDouble(), 0.0);
@@ -1164,6 +1164,20 @@ TEST(ValueTest, ToString)
     ASSERT_EQ(v.toString(), "-999999999999999999");
     ASSERT_EQ(utf8::utf16to8(v.toUtf16()), v.toString());
 
+    v = 2.0;
+    ASSERT_EQ(v.toString(), "2");
+    ASSERT_EQ(utf8::utf16to8(v.toUtf16()), v.toString());
+    v = 2.0f;
+    ASSERT_EQ(v.toString(), "2");
+    ASSERT_EQ(utf8::utf16to8(v.toUtf16()), v.toString());
+
+    v = -2.0;
+    ASSERT_EQ(v.toString(), "-2");
+    ASSERT_EQ(utf8::utf16to8(v.toUtf16()), v.toString());
+    v = -2.0f;
+    ASSERT_EQ(v.toString(), "-2");
+    ASSERT_EQ(utf8::utf16to8(v.toUtf16()), v.toString());
+
     v = 2.54;
     ASSERT_EQ(v.toString(), "2.54");
     ASSERT_EQ(utf8::utf16to8(v.toUtf16()), v.toString());
@@ -1178,6 +1192,29 @@ TEST(ValueTest, ToString)
     ASSERT_EQ(v.toString(), "-2.54");
     ASSERT_EQ(utf8::utf16to8(v.toUtf16()), v.toString());
 
+    v = 2550.625021000115;
+    ASSERT_EQ(v.toString(), "2550.625021000115");
+    ASSERT_EQ(utf8::utf16to8(v.toUtf16()), v.toString());
+
+    v = 2550.625021000115f;
+    ASSERT_EQ(v.toString(), "2550.625");
+    ASSERT_EQ(utf8::utf16to8(v.toUtf16()), v.toString());
+
+    v = -2550.625021000115;
+    ASSERT_EQ(v.toString(), "-2550.625021000115");
+    ASSERT_EQ(utf8::utf16to8(v.toUtf16()), v.toString());
+
+    v = -2550.625021000115f;
+    ASSERT_EQ(v.toString(), "-2550.625");
+    ASSERT_EQ(utf8::utf16to8(v.toUtf16()), v.toString());
+
+    v = 0.001;
+    ASSERT_EQ(v.toString(), "0.001");
+    ASSERT_EQ(utf8::utf16to8(v.toUtf16()), v.toString());
+    v = -0.001;
+    ASSERT_EQ(v.toString(), "-0.001");
+    ASSERT_EQ(utf8::utf16to8(v.toUtf16()), v.toString());
+
     v = false;
     ASSERT_EQ(v.toString(), "false");
     ASSERT_EQ(utf8::utf16to8(v.toUtf16()), v.toString());
@@ -1214,10 +1251,10 @@ TEST(ValueTest, ToString)
     ASSERT_EQ(utf8::utf16to8(v.toUtf16()), v.toString());
 
     v = "9432.4e+6";
-    ASSERT_EQ(v.toString(), "9.4324e+9");
+    ASSERT_EQ(v.toString(), "9432400000");
     ASSERT_EQ(utf8::utf16to8(v.toUtf16()), v.toString());
     v = "-9432.4e+6";
-    ASSERT_EQ(v.toString(), "-9.4324e+9");
+    ASSERT_EQ(v.toString(), "-9432400000");
     ASSERT_EQ(utf8::utf16to8(v.toUtf16()), v.toString());
 
     v = "false";

From 8bbe6243a6ed6dda32f01b3839dc17329f74448f Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Sun, 24 Mar 2024 16:16:09 +0100
Subject: [PATCH 49/77] fix #523: Refactor string conversion

---
 include/scratchcpp/value.h              | 237 ++++++++++++------
 src/blocks/looksblocks.cpp              |  13 +-
 src/blocks/soundblocks.cpp              |   6 +-
 src/internal/reader_common.h            |   8 +-
 src/scratch/list.cpp                    |   8 +-
 test/blocks/looks_blocks_test.cpp       |  93 ++++---
 test/blocks/sound_blocks_test.cpp       |   1 -
 test/load_project/load_project_test.cpp |   2 +-
 test/scratch_classes/value_test.cpp     | 312 ++++++++++++++++++++----
 9 files changed, 509 insertions(+), 171 deletions(-)

diff --git a/include/scratchcpp/value.h b/include/scratchcpp/value.h
index 8f52a872..3a6091a3 100644
--- a/include/scratchcpp/value.h
+++ b/include/scratchcpp/value.h
@@ -9,6 +9,7 @@
 #include <cassert>
 #include <iomanip>
 #include <utf8.h>
+#include <iostream>
 
 #include "global.h"
 
@@ -41,14 +42,28 @@ class LIBSCRATCHCPP_EXPORT Value
         Value(float numberValue) :
             m_type(Type::Double)
         {
-            m_doubleValue = floatToDouble(numberValue);
+            if (isInf(numberValue))
+                m_type = Type::Infinity;
+            else if (isNegativeInf(numberValue))
+                m_type = Type::NegativeInfinity;
+            else if (std::isnan(numberValue))
+                m_type = Type::NaN;
+            else
+                m_doubleValue = floatToDouble(numberValue);
         }
 
         /*! Constructs a number Value. */
         Value(double numberValue) :
             m_type(Type::Double)
         {
-            m_doubleValue = numberValue;
+            if (isInf(numberValue))
+                m_type = Type::Infinity;
+            else if (isNegativeInf(numberValue))
+                m_type = Type::NegativeInfinity;
+            else if (std::isnan(numberValue))
+                m_type = Type::NaN;
+            else
+                m_doubleValue = numberValue;
         }
 
         /*! Constructs a number Value. */
@@ -83,8 +98,14 @@ class LIBSCRATCHCPP_EXPORT Value
         Value(const std::string &stringValue) :
             m_type(Type::String)
         {
-            new (&m_stringValue) std::string(stringValue);
-            initString(stringValue);
+            if (stringValue == "Infinity")
+                m_type = Type::Infinity;
+            else if (stringValue == "-Infinity")
+                m_type = Type::NegativeInfinity;
+            else if (stringValue == "NaN")
+                m_type = Type::NaN;
+            else
+                new (&m_stringValue) std::string(stringValue);
         }
 
         /*! Constructs a string Value. */
@@ -140,17 +161,81 @@ class LIBSCRATCHCPP_EXPORT Value
         Type type() const { return m_type; }
 
         /*! Returns true if the value is infinity. */
-        bool isInfinity() const { return m_type == Type::Infinity; }
+        bool isInfinity() const
+        {
+            switch (m_type) {
+                case Type::Infinity:
+                    return true;
+                case Type::Integer:
+                    return isInf(m_intValue);
+                case Type::Double:
+                    return isInf(m_doubleValue);
+                case Type::String:
+                    return m_stringValue == "Infinity";
+                default:
+                    return false;
+            }
+        }
 
         /*! Returns true if the value is negative infinity. */
-        bool isNegativeInfinity() const { return m_type == Type::NegativeInfinity; }
+        bool isNegativeInfinity() const
+        {
+            switch (m_type) {
+                case Type::NegativeInfinity:
+                    return true;
+                case Type::Integer:
+                    return isNegativeInf(-m_intValue);
+                case Type::Double:
+                    return isNegativeInf(-m_doubleValue);
+                case Type::String:
+                    return m_stringValue == "-Infinity";
+                default:
+                    return false;
+            }
+        }
 
         /*! Returns true if the value is NaN (Not a Number). */
-        bool isNaN() const { return m_type == Type::NaN; }
+        bool isNaN() const
+        {
+            switch (m_type) {
+                case Type::NaN:
+                    return true;
+                case Type::Double:
+                    assert(!std::isnan(m_doubleValue));
+                    return std::isnan(m_doubleValue);
+                case Type::String:
+                    return m_stringValue == "NaN";
+                default:
+                    return false;
+            }
+        }
 
         /*! Returns true if the value is a number. */
         bool isNumber() const { return m_type == Type::Integer || m_type == Type::Double; }
 
+        /*! Returns true if the value is a number or can be converted to a number. */
+        bool isValidNumber() const
+        {
+            if (isNaN())
+                return false;
+
+            if (isInfinity() || isNegativeInfinity())
+                return true;
+
+            assert(m_type != Type::Infinity && m_type != Type::NegativeInfinity);
+
+            switch (m_type) {
+                case Type::Integer:
+                case Type::Double:
+                case Type::Bool:
+                    return true;
+                case Type::String:
+                    return m_stringValue.empty() || checkString(m_stringValue) > 0;
+                default:
+                    return false;
+            }
+        }
+
         /*! Returns true if the value is a boolean. */
         bool isBool() const { return m_type == Type::Bool; }
 
@@ -163,16 +248,9 @@ class LIBSCRATCHCPP_EXPORT Value
         /*! Returns the long representation of the value. */
         long toLong() const
         {
-            if (static_cast<int>(m_type) < 0) {
-                switch (m_type) {
-                    case Type::Infinity:
-                        return std::numeric_limits<long>::infinity();
-                    case Type::NegativeInfinity:
-                        return -std::numeric_limits<long>::infinity();
-                    default:
-                        return 0;
-                }
-            } else {
+            if (static_cast<int>(m_type) < 0)
+                return 0;
+            else {
                 switch (m_type) {
                     case Type::Integer:
                         return m_intValue;
@@ -376,8 +454,17 @@ class LIBSCRATCHCPP_EXPORT Value
             if (m_type == Type::String)
                 m_stringValue.~basic_string();
 
-            m_type = Type::Double;
-            m_doubleValue = floatToDouble(v);
+            if (isInf(v))
+                m_type = Type::Infinity;
+            else if (isNegativeInf(v))
+                m_type = Type::NegativeInfinity;
+            else if (std::isnan(v))
+                m_type = Type::NaN;
+            else {
+                m_type = Type::Double;
+                m_doubleValue = floatToDouble(v);
+            }
+
             return *this;
         }
 
@@ -386,8 +473,16 @@ class LIBSCRATCHCPP_EXPORT Value
             if (m_type == Type::String)
                 m_stringValue.~basic_string();
 
-            m_type = Type::Double;
-            m_doubleValue = v;
+            if (isInf(v))
+                m_type = Type::Infinity;
+            else if (isNegativeInf(v))
+                m_type = Type::NegativeInfinity;
+            else if (std::isnan(v))
+                m_type = Type::NaN;
+            else {
+                m_type = Type::Double;
+                m_doubleValue = v;
+            }
             return *this;
         }
 
@@ -423,30 +518,33 @@ class LIBSCRATCHCPP_EXPORT Value
 
         const Value &operator=(const std::string &v)
         {
-            if (m_type == Type::String)
-                m_stringValue = v;
-            else {
-                new (&m_stringValue) std::string(v);
-                m_type = Type::String;
-            }
+            if (v == "Infinity") {
+                if (m_type == Type::String)
+                    m_stringValue.~basic_string();
 
-            initString(v);
-            return *this;
-        }
+                m_type = Type::Infinity;
+            } else if (v == "-Infinity") {
+                if (m_type == Type::String)
+                    m_stringValue.~basic_string();
 
-        const Value &operator=(const char *v)
-        {
-            if (m_type == Type::String)
+                m_type = Type::NegativeInfinity;
+            } else if (v == "NaN") {
+                if (m_type == Type::String)
+                    m_stringValue.~basic_string();
+
+                m_type = Type::NaN;
+            } else if (m_type == Type::String)
                 m_stringValue = v;
             else {
                 new (&m_stringValue) std::string(v);
                 m_type = Type::String;
             }
 
-            initString(v);
             return *this;
         }
 
+        const Value &operator=(const char *v) { return (*this = std::string(v)); }
+
         const Value &operator=(const Value &v)
         {
             switch (v.m_type) {
@@ -496,59 +594,22 @@ class LIBSCRATCHCPP_EXPORT Value
                 std::string m_stringValue;
         };
 
-        // -1 - error
         // 0 - is string
         // 1 - is long
         // 2 - is double
-        static int checkString(const std::string &str, long *longValue, double *doubleValue)
+        static int checkString(const std::string &str)
         {
-            if (!longValue || !doubleValue)
-                return -1;
-
             bool ok;
 
             if ((str.find_first_of('.') == std::string::npos) && (str.find_first_of('e') == std::string::npos) && (str.find_first_of('E') == std::string::npos)) {
-                *longValue = stringToLong(str, &ok);
+                stringToLong(str, &ok);
                 return ok ? 1 : 0;
             } else {
-                *doubleValue = stringToDouble(str, &ok);
+                stringToDouble(str, &ok);
                 return ok ? 2 : 0;
             }
         }
 
-        void initString(const std::string &str)
-        {
-            if (str.empty())
-                return;
-            else if (str == "Infinity") {
-                m_type = Type::Infinity;
-                return;
-            } else if (str == "-Infinity") {
-                m_type = Type::NegativeInfinity;
-                return;
-            } else if (str == "NaN") {
-                m_type = Type::NaN;
-                return;
-            }
-
-            long l;
-            double d;
-            int type = checkString(str, &l, &d);
-
-            switch (type) {
-                case 1:
-                    *this = l;
-                    m_type = Type::Integer;
-                    break;
-                case 2:
-                    *this = d;
-                    m_type = Type::Double;
-                    break;
-                default:
-                    break;
-            }
-        }
-
         double getNumber(bool *ok = nullptr) const
         {
             // Equivalent to JavaScript Number(), *ok == false means NaN
@@ -589,8 +650,6 @@ class LIBSCRATCHCPP_EXPORT Value
             }
         }
 
-        void initString(const char *str) { initString(std::string(str)); }
-
         Type m_type;
 
         friend bool operator==(const Value &v1, const Value &v2)
@@ -922,7 +981,7 @@ class LIBSCRATCHCPP_EXPORT Value
                 }
             }
 
-            static const std::string digits = "0123456789+-";
+            static const std::string digits = "0123456789.eE+-";
 
             for (char c : s) {
                 if (digits.find(c) == std::string::npos) {
@@ -944,7 +1003,17 @@ class LIBSCRATCHCPP_EXPORT Value
         static std::string doubleToString(double v)
         {
             std::stringstream stream;
-            stream << std::setprecision(std::max(16u, digitCount(v))) << v;
+
+            if (v != 0) {
+                const int exponent = std::log10(std::abs(v));
+
+                if (exponent > 20)
+                    stream << std::scientific << std::setprecision(digitCount(v / std::pow(10, exponent + 1)) - 1) << v;
+                else
+                    stream << std::setprecision(std::max(16u, digitCount(v))) << v;
+            } else
+                stream << std::setprecision(std::max(16u, digitCount(v))) << v;
+
             std::string s = stream.str();
             std::size_t index;
 
@@ -989,6 +1058,18 @@ class LIBSCRATCHCPP_EXPORT Value
 
             return i + j;
         }
+
+        template<typename T>
+        static bool isInf(T v)
+        {
+            return v > 0 && std::isinf(v);
+        }
+
+        template<typename T>
+        static bool isNegativeInf(T v)
+        {
+            return v < 0 && std::isinf(v);
+        }
 };
 
 } // namespace libscratchcpp
diff --git a/src/blocks/looksblocks.cpp b/src/blocks/looksblocks.cpp
index cc1eb369..4c98e26b 100644
--- a/src/blocks/looksblocks.cpp
+++ b/src/blocks/looksblocks.cpp
@@ -350,6 +350,9 @@ void LooksBlocks::compileSwitchCostumeTo(Compiler *compiler)
                 if (v.type() == Value::Type::Integer) {
                     compiler->addConstValue(v.toLong() - 1);
                     compiler->addFunctionCall(&switchCostumeToByIndex);
+                } else {
+                    compiler->addInput(input);
+                    compiler->addFunctionCall(&switchCostumeTo);
                 }
             }
         } else {
@@ -394,6 +397,9 @@ void LooksBlocks::compileSwitchBackdropTo(Compiler *compiler)
                 if (v.type() == Value::Type::Integer) {
                     compiler->addConstValue(v.toLong() - 1);
                     compiler->addFunctionCall(&switchBackdropToByIndex);
+                } else {
+                    compiler->addInput(input);
+                    compiler->addFunctionCall(&switchBackdropTo);
                 }
             }
         } else {
@@ -433,6 +439,9 @@ void LooksBlocks::compileSwitchBackdropToAndWait(Compiler *compiler)
                 if (v.type() == Value::Type::Integer) {
                     compiler->addConstValue(v.toLong() - 1);
                     compiler->addFunctionCall(&switchBackdropToByIndexAndWait);
+                } else {
+                    compiler->addInput(input);
+                    compiler->addFunctionCall(&switchBackdropToAndWait);
                 }
             }
         } else {
@@ -936,7 +945,7 @@ unsigned int LooksBlocks::switchCostumeTo(VirtualMachine *vm)
         else if (nameStr == "previous costume")
             previousCostume(vm);
         else {
-            if (name->type() == Value::Type::Integer)
+            if (name->isValidNumber())
                 setCostumeByIndex(target, name->toLong() - 1);
         }
     } else
@@ -994,7 +1003,7 @@ void LooksBlocks::switchBackdropToImpl(VirtualMachine *vm)
         else if (nameStr == "random backdrop") {
             randomBackdropImpl(vm);
         } else {
-            if (name->type() == Value::Type::Integer)
+            if (name->isValidNumber())
                 setCostumeByIndex(stage, name->toLong() - 1);
         }
     } else
diff --git a/src/blocks/soundblocks.cpp b/src/blocks/soundblocks.cpp
index fab7302e..7106b6ca 100644
--- a/src/blocks/soundblocks.cpp
+++ b/src/blocks/soundblocks.cpp
@@ -66,7 +66,7 @@ bool SoundBlocks::compilePlayCommon(Compiler *compiler, bool untilDone, bool *by
         if (index == -1) {
             Value v(value);
 
-            if (v.type() == Value::Type::Integer) {
+            if (v.isValidNumber() && !v.isInfinity() && !v.isNegativeInfinity() && !(v.isString() && v.toString().empty())) {
                 compiler->addConstValue(v.toLong() - 1);
                 compiler->addFunctionCall(untilDone ? &playByIndexUntilDone : &playByIndex);
 
@@ -166,7 +166,7 @@ Sound *SoundBlocks::playCommon(VirtualMachine *vm)
             return sound;
         }
 
-        else if (name->type() == Value::Type::Integer) {
+        if (name->isValidNumber() && !name->isInfinity() && !name->isNegativeInfinity() && !(name->isString() && name->toString().empty())) {
             sound = getSoundByIndex(target, name->toLong() - 1);
 
             if (sound) {
@@ -245,7 +245,7 @@ unsigned int SoundBlocks::checkSound(VirtualMachine *vm)
     if (target) {
         Sound *sound = target->soundAt(target->findSound(name->toString())).get();
 
-        if (!sound && name->type() == Value::Type::Integer)
+        if (!sound && name->isValidNumber() && !name->isInfinity() && !name->isNegativeInfinity() && !(name->isString() && name->toString().empty()))
             sound = getSoundByIndex(target, name->toLong() - 1);
 
         if (sound) {
diff --git a/src/internal/reader_common.h b/src/internal/reader_common.h
index 840a1aa3..ac8bde38 100644
--- a/src/internal/reader_common.h
+++ b/src/internal/reader_common.h
@@ -15,8 +15,14 @@ Value jsonToValue(nlohmann::json value)
         return value.get<std::string>();
     else if (value.is_boolean())
         return value.get<bool>();
-    else
+    else if (value.is_number_integer() || value.is_number_unsigned())
+        return value.get<long>();
+    else if (value.is_number_float())
+        return value.get<double>();
+    else {
+        assert(!value.is_number());
         return value.dump();
+    }
 }
 
 } // namespace libscratchcpp
diff --git a/src/scratch/list.cpp b/src/scratch/list.cpp
index f2e2c16b..043077ba 100644
--- a/src/scratch/list.cpp
+++ b/src/scratch/list.cpp
@@ -74,9 +74,15 @@ std::string List::toString() const
     bool digits = true;
 
     for (const auto &item : *this) {
-        if (item.type() == Value::Type::Integer) {
+        if (item.isValidNumber() && !item.toString().empty()) {
+            double doubleNum = item.toDouble();
             long num = item.toLong();
 
+            if (doubleNum != num) {
+                digits = false;
+                break;
+            }
+
             if (num < 0 || num >= 10) {
                 digits = false;
                 break;
diff --git a/test/blocks/looks_blocks_test.cpp b/test/blocks/looks_blocks_test.cpp
index 288c4781..2bf1e44a 100644
--- a/test/blocks/looks_blocks_test.cpp
+++ b/test/blocks/looks_blocks_test.cpp
@@ -1469,6 +1469,7 @@ TEST_F(LooksBlocksTest, SwitchCostumeTo)
 
     // Test without any costumes first
     compiler.setBlock(block1);
+    EXPECT_CALL(m_engineMock, functionIndex(&LooksBlocks::switchCostumeTo)).WillOnce(Return(3));
     LooksBlocks::compileSwitchCostumeTo(&compiler);
 
     target.addCostume(std::make_shared<Costume>("costume1", "c1", "svg"));
@@ -1480,19 +1481,19 @@ TEST_F(LooksBlocksTest, SwitchCostumeTo)
 
     target.addCostume(std::make_shared<Costume>("costume3", "c3", "svg"));
 
-    EXPECT_CALL(m_engineMock, functionIndex(&LooksBlocks::switchCostumeToByIndex)).WillOnce(Return(0));
+    EXPECT_CALL(m_engineMock, functionIndex(&LooksBlocks::switchCostumeTo)).WillOnce(Return(3));
     compiler.setBlock(block2);
     LooksBlocks::compileSwitchCostumeTo(&compiler);
 
-    EXPECT_CALL(m_engineMock, functionIndex(&LooksBlocks::switchCostumeToByIndex)).WillOnce(Return(0));
+    EXPECT_CALL(m_engineMock, functionIndex(&LooksBlocks::switchCostumeTo)).WillOnce(Return(3));
     compiler.setBlock(block3);
     LooksBlocks::compileSwitchCostumeTo(&compiler);
 
-    EXPECT_CALL(m_engineMock, functionIndex(&LooksBlocks::switchCostumeToByIndex)).WillOnce(Return(0));
+    EXPECT_CALL(m_engineMock, functionIndex(&LooksBlocks::switchCostumeTo)).WillOnce(Return(3));
     compiler.setBlock(block4);
     LooksBlocks::compileSwitchCostumeTo(&compiler);
 
-    EXPECT_CALL(m_engineMock, functionIndex(&LooksBlocks::switchCostumeToByIndex)).WillOnce(Return(0));
+    EXPECT_CALL(m_engineMock, functionIndex(&LooksBlocks::switchCostumeTo)).WillOnce(Return(3));
     compiler.setBlock(block5);
     LooksBlocks::compileSwitchCostumeTo(&compiler);
 
@@ -1535,7 +1536,7 @@ TEST_F(LooksBlocksTest, SwitchCostumeTo)
               vm::OP_CONST,
               0,
               vm::OP_EXEC,
-              0,
+              3,
               vm::OP_CONST,
               1,
               vm::OP_EXEC,
@@ -1543,36 +1544,40 @@ TEST_F(LooksBlocksTest, SwitchCostumeTo)
               vm::OP_CONST,
               2,
               vm::OP_EXEC,
-              0,
+              3,
               vm::OP_CONST,
               3,
               vm::OP_EXEC,
-              0,
+              3,
               vm::OP_CONST,
               4,
               vm::OP_EXEC,
-              0,
+              3,
               vm::OP_CONST,
               5,
               vm::OP_EXEC,
+              3,
+              vm::OP_CONST,
+              6,
+              vm::OP_EXEC,
               0,
               vm::OP_EXEC,
               1,
               vm::OP_CONST,
-              6,
+              7,
               vm::OP_EXEC,
               0,
               vm::OP_EXEC,
               2,
               vm::OP_CONST,
-              7,
+              8,
               vm::OP_EXEC,
               0,
               vm::OP_NULL,
               vm::OP_EXEC,
               3,
               vm::OP_HALT }));
-    ASSERT_EQ(compiler.constValues(), std::vector<Value>({ 1, -1, 0, 1, 3, 3, 4, 5 }));
+    ASSERT_EQ(compiler.constValues(), std::vector<Value>({ "costume2", 1, "0", "1", "2", "4", 3, 4, 5 }));
 }
 
 TEST_F(LooksBlocksTest, SwitchCostumeToImpl)
@@ -1887,6 +1892,7 @@ TEST_F(LooksBlocksTest, SwitchBackdropTo)
 
     // Test without any backdrops first
     EXPECT_CALL(m_engineMock, stage()).WillOnce(Return(&stage));
+    EXPECT_CALL(m_engineMock, functionIndex(&LooksBlocks::switchBackdropTo)).WillOnce(Return(4));
     compiler.setBlock(block1);
     LooksBlocks::compileSwitchBackdropTo(&compiler);
 
@@ -1901,22 +1907,22 @@ TEST_F(LooksBlocksTest, SwitchBackdropTo)
     stage.addCostume(std::make_shared<Costume>("backdrop3", "b3", "svg"));
 
     EXPECT_CALL(m_engineMock, stage()).WillOnce(Return(&stage));
-    EXPECT_CALL(m_engineMock, functionIndex(&LooksBlocks::switchBackdropToByIndex)).WillOnce(Return(0));
+    EXPECT_CALL(m_engineMock, functionIndex(&LooksBlocks::switchBackdropTo)).WillOnce(Return(4));
     compiler.setBlock(block2);
     LooksBlocks::compileSwitchBackdropTo(&compiler);
 
     EXPECT_CALL(m_engineMock, stage()).WillOnce(Return(&stage));
-    EXPECT_CALL(m_engineMock, functionIndex(&LooksBlocks::switchBackdropToByIndex)).WillOnce(Return(0));
+    EXPECT_CALL(m_engineMock, functionIndex(&LooksBlocks::switchBackdropTo)).WillOnce(Return(4));
     compiler.setBlock(block3);
     LooksBlocks::compileSwitchBackdropTo(&compiler);
 
     EXPECT_CALL(m_engineMock, stage()).WillOnce(Return(&stage));
-    EXPECT_CALL(m_engineMock, functionIndex(&LooksBlocks::switchBackdropToByIndex)).WillOnce(Return(0));
+    EXPECT_CALL(m_engineMock, functionIndex(&LooksBlocks::switchBackdropTo)).WillOnce(Return(4));
     compiler.setBlock(block4);
     LooksBlocks::compileSwitchBackdropTo(&compiler);
 
     EXPECT_CALL(m_engineMock, stage()).WillOnce(Return(&stage));
-    EXPECT_CALL(m_engineMock, functionIndex(&LooksBlocks::switchBackdropToByIndex)).WillOnce(Return(0));
+    EXPECT_CALL(m_engineMock, functionIndex(&LooksBlocks::switchBackdropTo)).WillOnce(Return(4));
     compiler.setBlock(block5);
     LooksBlocks::compileSwitchBackdropTo(&compiler);
 
@@ -1977,7 +1983,7 @@ TEST_F(LooksBlocksTest, SwitchBackdropTo)
               vm::OP_CONST,
               0,
               vm::OP_EXEC,
-              0,
+              4,
               vm::OP_CONST,
               1,
               vm::OP_EXEC,
@@ -1985,42 +1991,46 @@ TEST_F(LooksBlocksTest, SwitchBackdropTo)
               vm::OP_CONST,
               2,
               vm::OP_EXEC,
-              0,
+              4,
               vm::OP_CONST,
               3,
               vm::OP_EXEC,
-              0,
+              4,
               vm::OP_CONST,
               4,
               vm::OP_EXEC,
-              0,
+              4,
               vm::OP_CONST,
               5,
               vm::OP_EXEC,
+              4,
+              vm::OP_CONST,
+              6,
+              vm::OP_EXEC,
               0,
               vm::OP_EXEC,
               1,
               vm::OP_CONST,
-              6,
+              7,
               vm::OP_EXEC,
               0,
               vm::OP_EXEC,
               2,
               vm::OP_CONST,
-              7,
+              8,
               vm::OP_EXEC,
               0,
               vm::OP_EXEC,
               3,
               vm::OP_CONST,
-              8,
+              9,
               vm::OP_EXEC,
               0,
               vm::OP_NULL,
               vm::OP_EXEC,
               4,
               vm::OP_HALT }));
-    ASSERT_EQ(compiler.constValues(), std::vector<Value>({ 1, -1, 0, 1, 3, 3, 4, 5, 6 }));
+    ASSERT_EQ(compiler.constValues(), std::vector<Value>({ "backdrop2", 1, "0", "1", "2", "4", 3, 4, 5, 6 }));
 }
 
 TEST_F(LooksBlocksTest, SwitchBackdropToImpl)
@@ -2294,6 +2304,7 @@ TEST_F(LooksBlocksTest, SwitchBackdropToAndWait)
 
     // Test without any backdrops first
     EXPECT_CALL(m_engineMock, stage()).WillOnce(Return(&stage));
+    EXPECT_CALL(m_engineMock, functionIndex(&LooksBlocks::switchBackdropToAndWait)).WillOnce(Return(4));
     EXPECT_CALL(m_engineMock, functionIndex(&LooksBlocks::backdropNumber)).WillOnce(Return(6));
     EXPECT_CALL(m_engineMock, functionIndex(&LooksBlocks::checkBackdropScripts)).WillOnce(Return(5));
     compiler.setBlock(block1);
@@ -2312,28 +2323,28 @@ TEST_F(LooksBlocksTest, SwitchBackdropToAndWait)
     stage.addCostume(std::make_shared<Costume>("backdrop3", "b3", "svg"));
 
     EXPECT_CALL(m_engineMock, stage()).WillOnce(Return(&stage));
-    EXPECT_CALL(m_engineMock, functionIndex(&LooksBlocks::switchBackdropToByIndexAndWait)).WillOnce(Return(0));
+    EXPECT_CALL(m_engineMock, functionIndex(&LooksBlocks::switchBackdropToAndWait)).WillOnce(Return(4));
     EXPECT_CALL(m_engineMock, functionIndex(&LooksBlocks::backdropNumber)).WillOnce(Return(6));
     EXPECT_CALL(m_engineMock, functionIndex(&LooksBlocks::checkBackdropScripts)).WillOnce(Return(5));
     compiler.setBlock(block2);
     LooksBlocks::compileSwitchBackdropToAndWait(&compiler);
 
     EXPECT_CALL(m_engineMock, stage()).WillOnce(Return(&stage));
-    EXPECT_CALL(m_engineMock, functionIndex(&LooksBlocks::switchBackdropToByIndexAndWait)).WillOnce(Return(0));
+    EXPECT_CALL(m_engineMock, functionIndex(&LooksBlocks::switchBackdropToAndWait)).WillOnce(Return(4));
     EXPECT_CALL(m_engineMock, functionIndex(&LooksBlocks::backdropNumber)).WillOnce(Return(6));
     EXPECT_CALL(m_engineMock, functionIndex(&LooksBlocks::checkBackdropScripts)).WillOnce(Return(5));
     compiler.setBlock(block3);
     LooksBlocks::compileSwitchBackdropToAndWait(&compiler);
 
     EXPECT_CALL(m_engineMock, stage()).WillOnce(Return(&stage));
-    EXPECT_CALL(m_engineMock, functionIndex(&LooksBlocks::switchBackdropToByIndexAndWait)).WillOnce(Return(0));
+    EXPECT_CALL(m_engineMock, functionIndex(&LooksBlocks::switchBackdropToAndWait)).WillOnce(Return(4));
     EXPECT_CALL(m_engineMock, functionIndex(&LooksBlocks::backdropNumber)).WillOnce(Return(6));
     EXPECT_CALL(m_engineMock, functionIndex(&LooksBlocks::checkBackdropScripts)).WillOnce(Return(5));
     compiler.setBlock(block4);
     LooksBlocks::compileSwitchBackdropToAndWait(&compiler);
 
     EXPECT_CALL(m_engineMock, stage()).WillOnce(Return(&stage));
-    EXPECT_CALL(m_engineMock, functionIndex(&LooksBlocks::switchBackdropToByIndexAndWait)).WillOnce(Return(0));
+    EXPECT_CALL(m_engineMock, functionIndex(&LooksBlocks::switchBackdropToAndWait)).WillOnce(Return(4));
     EXPECT_CALL(m_engineMock, functionIndex(&LooksBlocks::backdropNumber)).WillOnce(Return(6));
     EXPECT_CALL(m_engineMock, functionIndex(&LooksBlocks::checkBackdropScripts)).WillOnce(Return(5));
     compiler.setBlock(block5);
@@ -2409,14 +2420,10 @@ TEST_F(LooksBlocksTest, SwitchBackdropToAndWait)
         compiler.bytecode(),
         std::vector<unsigned int>(
             { vm::OP_START,
-              vm::OP_EXEC,
-              6,
-              vm::OP_EXEC,
-              5,
               vm::OP_CONST,
               0,
               vm::OP_EXEC,
-              0,
+              4,
               vm::OP_EXEC,
               6,
               vm::OP_EXEC,
@@ -2432,7 +2439,7 @@ TEST_F(LooksBlocksTest, SwitchBackdropToAndWait)
               vm::OP_CONST,
               2,
               vm::OP_EXEC,
-              0,
+              4,
               vm::OP_EXEC,
               6,
               vm::OP_EXEC,
@@ -2440,7 +2447,7 @@ TEST_F(LooksBlocksTest, SwitchBackdropToAndWait)
               vm::OP_CONST,
               3,
               vm::OP_EXEC,
-              0,
+              4,
               vm::OP_EXEC,
               6,
               vm::OP_EXEC,
@@ -2448,7 +2455,7 @@ TEST_F(LooksBlocksTest, SwitchBackdropToAndWait)
               vm::OP_CONST,
               4,
               vm::OP_EXEC,
-              0,
+              4,
               vm::OP_EXEC,
               6,
               vm::OP_EXEC,
@@ -2456,6 +2463,14 @@ TEST_F(LooksBlocksTest, SwitchBackdropToAndWait)
               vm::OP_CONST,
               5,
               vm::OP_EXEC,
+              4,
+              vm::OP_EXEC,
+              6,
+              vm::OP_EXEC,
+              5,
+              vm::OP_CONST,
+              6,
+              vm::OP_EXEC,
               0,
               vm::OP_EXEC,
               6,
@@ -2468,7 +2483,7 @@ TEST_F(LooksBlocksTest, SwitchBackdropToAndWait)
               vm::OP_EXEC,
               5,
               vm::OP_CONST,
-              6,
+              7,
               vm::OP_EXEC,
               0,
               vm::OP_EXEC,
@@ -2482,7 +2497,7 @@ TEST_F(LooksBlocksTest, SwitchBackdropToAndWait)
               vm::OP_EXEC,
               5,
               vm::OP_CONST,
-              7,
+              8,
               vm::OP_EXEC,
               0,
               vm::OP_EXEC,
@@ -2496,7 +2511,7 @@ TEST_F(LooksBlocksTest, SwitchBackdropToAndWait)
               vm::OP_EXEC,
               5,
               vm::OP_CONST,
-              8,
+              9,
               vm::OP_EXEC,
               0,
               vm::OP_EXEC,
@@ -2511,7 +2526,7 @@ TEST_F(LooksBlocksTest, SwitchBackdropToAndWait)
               vm::OP_EXEC,
               5,
               vm::OP_HALT }));
-    ASSERT_EQ(compiler.constValues(), std::vector<Value>({ 1, -1, 0, 1, 3, 3, 4, 5, 6 }));
+    ASSERT_EQ(compiler.constValues(), std::vector<Value>({ "backdrop2", 1, "0", "1", "2", "4", 3, 4, 5, 6 }));
 }
 
 TEST_F(LooksBlocksTest, SwitchBackdropToAndWaitImpl)
diff --git a/test/blocks/sound_blocks_test.cpp b/test/blocks/sound_blocks_test.cpp
index d533faed..c5b2b366 100644
--- a/test/blocks/sound_blocks_test.cpp
+++ b/test/blocks/sound_blocks_test.cpp
@@ -172,7 +172,6 @@ TEST_F(SoundBlocksTest, Play)
     SoundBlocks::compilePlay(&compiler);
 
     EXPECT_CALL(m_engineMock, functionIndex(&SoundBlocks::playByIndex)).Times(0);
-    EXPECT_CALL(m_engineMock, functionIndex(&SoundBlocks::play)).Times(0);
     compiler.setBlock(block5);
     SoundBlocks::compilePlay(&compiler);
 
diff --git a/test/load_project/load_project_test.cpp b/test/load_project/load_project_test.cpp
index 767a5feb..827884e5 100644
--- a/test/load_project/load_project_test.cpp
+++ b/test/load_project/load_project_test.cpp
@@ -757,5 +757,5 @@ TEST(LoadProjectTest, LoadScientificNotationNumber)
     ASSERT_EQ(GET_VAR(stage, "test1")->value().toDouble(), 5000);
 
     ASSERT_VAR(stage, "test2");
-    ASSERT_EQ(GET_VAR(stage, "test2")->value().toDouble(), 0);
+    ASSERT_EQ(GET_VAR(stage, "test2")->value().toDouble(), 4e-323);
 }
diff --git a/test/scratch_classes/value_test.cpp b/test/scratch_classes/value_test.cpp
index 31c91bdb..4ac41c09 100644
--- a/test/scratch_classes/value_test.cpp
+++ b/test/scratch_classes/value_test.cpp
@@ -13,34 +13,113 @@ TEST(ValueTest, DefaultConstructor)
     ASSERT_FALSE(v.isNegativeInfinity());
     ASSERT_FALSE(v.isNaN());
     ASSERT_TRUE(v.isNumber());
+    ASSERT_TRUE(v.isValidNumber());
     ASSERT_FALSE(v.isBool());
     ASSERT_FALSE(v.isString());
 }
 
 TEST(ValueTest, FloatConstructor)
 {
-    Value v(3.14f);
-    ASSERT_EQ(v.toDouble(), 3.14);
-    ASSERT_EQ(v.type(), Value::Type::Double);
-    ASSERT_FALSE(v.isInfinity());
-    ASSERT_FALSE(v.isNegativeInfinity());
-    ASSERT_FALSE(v.isNaN());
-    ASSERT_TRUE(v.isNumber());
-    ASSERT_FALSE(v.isBool());
-    ASSERT_FALSE(v.isString());
+    {
+        Value v(3.14f);
+        ASSERT_EQ(v.toDouble(), 3.14);
+        ASSERT_EQ(v.type(), Value::Type::Double);
+        ASSERT_FALSE(v.isInfinity());
+        ASSERT_FALSE(v.isNegativeInfinity());
+        ASSERT_FALSE(v.isNaN());
+        ASSERT_TRUE(v.isNumber());
+        ASSERT_TRUE(v.isValidNumber());
+        ASSERT_FALSE(v.isBool());
+        ASSERT_FALSE(v.isString());
+    }
+
+    {
+        Value v(std::numeric_limits<float>::infinity());
+        ASSERT_EQ(v.type(), Value::Type::Infinity);
+        ASSERT_TRUE(v.isInfinity());
+        ASSERT_FALSE(v.isNegativeInfinity());
+        ASSERT_FALSE(v.isNaN());
+        ASSERT_FALSE(v.isNumber());
+        ASSERT_TRUE(v.isValidNumber());
+        ASSERT_FALSE(v.isBool());
+        ASSERT_FALSE(v.isString());
+    }
+
+    {
+        Value v(-std::numeric_limits<float>::infinity());
+        ASSERT_EQ(v.type(), Value::Type::NegativeInfinity);
+        ASSERT_FALSE(v.isInfinity());
+        ASSERT_TRUE(v.isNegativeInfinity());
+        ASSERT_FALSE(v.isNaN());
+        ASSERT_FALSE(v.isNumber());
+        ASSERT_TRUE(v.isValidNumber());
+        ASSERT_FALSE(v.isBool());
+        ASSERT_FALSE(v.isString());
+    }
+
+    {
+        Value v(std::numeric_limits<float>::quiet_NaN());
+        ASSERT_EQ(v.type(), Value::Type::NaN);
+        ASSERT_FALSE(v.isInfinity());
+        ASSERT_FALSE(v.isNegativeInfinity());
+        ASSERT_TRUE(v.isNaN());
+        ASSERT_FALSE(v.isNumber());
+        ASSERT_FALSE(v.isValidNumber());
+        ASSERT_FALSE(v.isBool());
+        ASSERT_FALSE(v.isString());
+    }
 }
 
 TEST(ValueTest, DoubleConstructor)
 {
-    Value v(static_cast<double>(3.14));
-    ASSERT_EQ(v.toDouble(), 3.14);
-    ASSERT_EQ(v.type(), Value::Type::Double);
-    ASSERT_FALSE(v.isInfinity());
-    ASSERT_FALSE(v.isNegativeInfinity());
-    ASSERT_FALSE(v.isNaN());
-    ASSERT_TRUE(v.isNumber());
-    ASSERT_FALSE(v.isBool());
-    ASSERT_FALSE(v.isString());
+    {
+        Value v(static_cast<double>(3.14));
+        ASSERT_EQ(v.toDouble(), 3.14);
+        ASSERT_EQ(v.type(), Value::Type::Double);
+        ASSERT_FALSE(v.isInfinity());
+        ASSERT_FALSE(v.isNegativeInfinity());
+        ASSERT_FALSE(v.isNaN());
+        ASSERT_TRUE(v.isNumber());
+        ASSERT_TRUE(v.isValidNumber());
+        ASSERT_FALSE(v.isBool());
+        ASSERT_FALSE(v.isString());
+    }
+
+    {
+        Value v(std::numeric_limits<double>::infinity());
+        ASSERT_EQ(v.type(), Value::Type::Infinity);
+        ASSERT_TRUE(v.isInfinity());
+        ASSERT_FALSE(v.isNegativeInfinity());
+        ASSERT_FALSE(v.isNaN());
+        ASSERT_FALSE(v.isNumber());
+        ASSERT_TRUE(v.isValidNumber());
+        ASSERT_FALSE(v.isBool());
+        ASSERT_FALSE(v.isString());
+    }
+
+    {
+        Value v(-std::numeric_limits<double>::infinity());
+        ASSERT_EQ(v.type(), Value::Type::NegativeInfinity);
+        ASSERT_FALSE(v.isInfinity());
+        ASSERT_TRUE(v.isNegativeInfinity());
+        ASSERT_FALSE(v.isNaN());
+        ASSERT_FALSE(v.isNumber());
+        ASSERT_TRUE(v.isValidNumber());
+        ASSERT_FALSE(v.isBool());
+        ASSERT_FALSE(v.isString());
+    }
+
+    {
+        Value v(std::numeric_limits<double>::quiet_NaN());
+        ASSERT_EQ(v.type(), Value::Type::NaN);
+        ASSERT_FALSE(v.isInfinity());
+        ASSERT_FALSE(v.isNegativeInfinity());
+        ASSERT_TRUE(v.isNaN());
+        ASSERT_FALSE(v.isNumber());
+        ASSERT_FALSE(v.isValidNumber());
+        ASSERT_FALSE(v.isBool());
+        ASSERT_FALSE(v.isString());
+    }
 }
 
 TEST(ValueTest, IntConstructor)
@@ -52,6 +131,7 @@ TEST(ValueTest, IntConstructor)
     ASSERT_FALSE(v.isNegativeInfinity());
     ASSERT_FALSE(v.isNaN());
     ASSERT_TRUE(v.isNumber());
+    ASSERT_TRUE(v.isValidNumber());
     ASSERT_FALSE(v.isBool());
     ASSERT_FALSE(v.isString());
 }
@@ -65,6 +145,7 @@ TEST(ValueTest, SizeTConstructor)
     ASSERT_FALSE(v.isNegativeInfinity());
     ASSERT_FALSE(v.isNaN());
     ASSERT_TRUE(v.isNumber());
+    ASSERT_TRUE(v.isValidNumber());
     ASSERT_FALSE(v.isBool());
     ASSERT_FALSE(v.isString());
 }
@@ -78,6 +159,7 @@ TEST(ValueTest, LongConstructor)
     ASSERT_FALSE(v.isNegativeInfinity());
     ASSERT_FALSE(v.isNaN());
     ASSERT_TRUE(v.isNumber());
+    ASSERT_TRUE(v.isValidNumber());
     ASSERT_FALSE(v.isBool());
     ASSERT_FALSE(v.isString());
 }
@@ -92,6 +174,7 @@ TEST(ValueTest, BoolConstructor)
         ASSERT_FALSE(v.isNegativeInfinity());
         ASSERT_FALSE(v.isNaN());
         ASSERT_FALSE(v.isNumber());
+        ASSERT_TRUE(v.isValidNumber());
         ASSERT_TRUE(v.isBool());
         ASSERT_FALSE(v.isString());
     }
@@ -104,6 +187,7 @@ TEST(ValueTest, BoolConstructor)
         ASSERT_FALSE(v.isNegativeInfinity());
         ASSERT_FALSE(v.isNaN());
         ASSERT_FALSE(v.isNumber());
+        ASSERT_TRUE(v.isValidNumber());
         ASSERT_TRUE(v.isBool());
         ASSERT_FALSE(v.isString());
     }
@@ -119,6 +203,7 @@ TEST(ValueTest, StdStringConstructor)
         ASSERT_FALSE(v.isNegativeInfinity());
         ASSERT_FALSE(v.isNaN());
         ASSERT_FALSE(v.isNumber());
+        ASSERT_FALSE(v.isValidNumber());
         ASSERT_FALSE(v.isBool());
         ASSERT_TRUE(v.isString());
     }
@@ -126,31 +211,28 @@ TEST(ValueTest, StdStringConstructor)
     {
         Value v(std::string("532"));
         ASSERT_EQ(v.toString(), "532");
-        ASSERT_EQ(v.type(), Value::Type::Integer);
+        ASSERT_EQ(v.type(), Value::Type::String);
         ASSERT_FALSE(v.isInfinity());
         ASSERT_FALSE(v.isNegativeInfinity());
         ASSERT_FALSE(v.isNaN());
-        ASSERT_TRUE(v.isNumber());
+        ASSERT_FALSE(v.isNumber());
+        ASSERT_TRUE(v.isValidNumber());
         ASSERT_FALSE(v.isBool());
-        ASSERT_FALSE(v.isString());
+        ASSERT_TRUE(v.isString());
     }
 
     {
-        std::string oldLocale = std::setlocale(LC_NUMERIC, nullptr);
-        std::setlocale(LC_NUMERIC, "sk_SK.UTF-8");
-
         Value v(std::string("532.15"));
 
-        std::setlocale(LC_NUMERIC, oldLocale.c_str());
-
         ASSERT_EQ(v.toString(), "532.15");
-        ASSERT_EQ(v.type(), Value::Type::Double);
+        ASSERT_EQ(v.type(), Value::Type::String);
         ASSERT_FALSE(v.isInfinity());
         ASSERT_FALSE(v.isNegativeInfinity());
         ASSERT_FALSE(v.isNaN());
-        ASSERT_TRUE(v.isNumber());
+        ASSERT_FALSE(v.isNumber());
+        ASSERT_TRUE(v.isValidNumber());
         ASSERT_FALSE(v.isBool());
-        ASSERT_FALSE(v.isString());
+        ASSERT_TRUE(v.isString());
     }
 
     {
@@ -161,42 +243,71 @@ TEST(ValueTest, StdStringConstructor)
         ASSERT_FALSE(v.isNegativeInfinity());
         ASSERT_FALSE(v.isNaN());
         ASSERT_FALSE(v.isNumber());
+        ASSERT_FALSE(v.isValidNumber());
+        ASSERT_FALSE(v.isBool());
+        ASSERT_TRUE(v.isString());
+    }
+
+    {
+        Value v(std::string(""));
+
+        ASSERT_EQ(v.toString(), "");
+        ASSERT_EQ(v.type(), Value::Type::String);
+        ASSERT_FALSE(v.isInfinity());
+        ASSERT_FALSE(v.isNegativeInfinity());
+        ASSERT_FALSE(v.isNaN());
+        ASSERT_FALSE(v.isNumber());
+        ASSERT_TRUE(v.isValidNumber());
+        ASSERT_FALSE(v.isBool());
+        ASSERT_TRUE(v.isString());
+    }
+
+    {
+        Value v(std::string(" "));
+
+        ASSERT_EQ(v.toString(), " ");
+        ASSERT_EQ(v.type(), Value::Type::String);
+        ASSERT_FALSE(v.isInfinity());
+        ASSERT_FALSE(v.isNegativeInfinity());
+        ASSERT_FALSE(v.isNaN());
+        ASSERT_FALSE(v.isNumber());
+        ASSERT_FALSE(v.isValidNumber());
         ASSERT_FALSE(v.isBool());
         ASSERT_TRUE(v.isString());
     }
 
     {
         Value v(std::string("Infinity"));
-        ASSERT_EQ(v.toString(), "Infinity");
         ASSERT_EQ(v.type(), Value::Type::Infinity);
         ASSERT_TRUE(v.isInfinity());
         ASSERT_FALSE(v.isNegativeInfinity());
         ASSERT_FALSE(v.isNaN());
         ASSERT_FALSE(v.isNumber());
+        ASSERT_TRUE(v.isValidNumber());
         ASSERT_FALSE(v.isBool());
         ASSERT_FALSE(v.isString());
     }
 
     {
         Value v(std::string("-Infinity"));
-        ASSERT_EQ(v.toString(), "-Infinity");
         ASSERT_EQ(v.type(), Value::Type::NegativeInfinity);
         ASSERT_FALSE(v.isInfinity());
         ASSERT_TRUE(v.isNegativeInfinity());
         ASSERT_FALSE(v.isNaN());
         ASSERT_FALSE(v.isNumber());
+        ASSERT_TRUE(v.isValidNumber());
         ASSERT_FALSE(v.isBool());
         ASSERT_FALSE(v.isString());
     }
 
     {
         Value v(std::string("NaN"));
-        ASSERT_EQ(v.toString(), "NaN");
         ASSERT_EQ(v.type(), Value::Type::NaN);
         ASSERT_FALSE(v.isInfinity());
         ASSERT_FALSE(v.isNegativeInfinity());
         ASSERT_TRUE(v.isNaN());
         ASSERT_FALSE(v.isNumber());
+        ASSERT_FALSE(v.isValidNumber());
         ASSERT_FALSE(v.isBool());
         ASSERT_FALSE(v.isString());
     }
@@ -212,42 +323,43 @@ TEST(ValueTest, CStringConstructor)
         ASSERT_FALSE(v.isNegativeInfinity());
         ASSERT_FALSE(v.isNaN());
         ASSERT_FALSE(v.isNumber());
+        ASSERT_FALSE(v.isValidNumber());
         ASSERT_FALSE(v.isBool());
         ASSERT_TRUE(v.isString());
     }
 
     {
         Value v("Infinity");
-        ASSERT_EQ(v.toString(), "Infinity");
         ASSERT_EQ(v.type(), Value::Type::Infinity);
         ASSERT_TRUE(v.isInfinity());
         ASSERT_FALSE(v.isNegativeInfinity());
         ASSERT_FALSE(v.isNaN());
         ASSERT_FALSE(v.isNumber());
+        ASSERT_TRUE(v.isValidNumber());
         ASSERT_FALSE(v.isBool());
         ASSERT_FALSE(v.isString());
     }
 
     {
         Value v("-Infinity");
-        ASSERT_EQ(v.toString(), "-Infinity");
         ASSERT_EQ(v.type(), Value::Type::NegativeInfinity);
         ASSERT_FALSE(v.isInfinity());
         ASSERT_TRUE(v.isNegativeInfinity());
         ASSERT_FALSE(v.isNaN());
         ASSERT_FALSE(v.isNumber());
+        ASSERT_TRUE(v.isValidNumber());
         ASSERT_FALSE(v.isBool());
         ASSERT_FALSE(v.isString());
     }
 
     {
         Value v("NaN");
-        ASSERT_EQ(v.toString(), "NaN");
         ASSERT_EQ(v.type(), Value::Type::NaN);
         ASSERT_FALSE(v.isInfinity());
         ASSERT_FALSE(v.isNegativeInfinity());
         ASSERT_TRUE(v.isNaN());
         ASSERT_FALSE(v.isNumber());
+        ASSERT_FALSE(v.isValidNumber());
         ASSERT_FALSE(v.isBool());
         ASSERT_FALSE(v.isString());
     }
@@ -261,6 +373,7 @@ TEST(ValueTest, InfinityConstructor)
     ASSERT_FALSE(v.isNegativeInfinity());
     ASSERT_FALSE(v.isNaN());
     ASSERT_FALSE(v.isNumber());
+    ASSERT_TRUE(v.isValidNumber());
     ASSERT_FALSE(v.isBool());
     ASSERT_FALSE(v.isString());
 }
@@ -273,6 +386,7 @@ TEST(ValueTest, NegativeInfinityConstructor)
     ASSERT_TRUE(v.isNegativeInfinity());
     ASSERT_FALSE(v.isNaN());
     ASSERT_FALSE(v.isNumber());
+    ASSERT_TRUE(v.isValidNumber());
     ASSERT_FALSE(v.isBool());
     ASSERT_FALSE(v.isString());
 }
@@ -285,6 +399,7 @@ TEST(ValueTest, NaNConstructor)
     ASSERT_FALSE(v.isNegativeInfinity());
     ASSERT_TRUE(v.isNaN());
     ASSERT_FALSE(v.isNumber());
+    ASSERT_FALSE(v.isValidNumber());
     ASSERT_FALSE(v.isBool());
     ASSERT_FALSE(v.isString());
 }
@@ -300,6 +415,7 @@ TEST(ValueTest, CopyConstructor)
         ASSERT_FALSE(v2.isNegativeInfinity());
         ASSERT_FALSE(v2.isNaN());
         ASSERT_TRUE(v2.isNumber());
+        ASSERT_TRUE(v2.isValidNumber());
         ASSERT_FALSE(v2.isBool());
         ASSERT_FALSE(v2.isString());
     }
@@ -313,6 +429,7 @@ TEST(ValueTest, CopyConstructor)
         ASSERT_FALSE(v2.isNegativeInfinity());
         ASSERT_FALSE(v2.isNaN());
         ASSERT_TRUE(v2.isNumber());
+        ASSERT_TRUE(v2.isValidNumber());
         ASSERT_FALSE(v2.isBool());
         ASSERT_FALSE(v2.isString());
     }
@@ -326,6 +443,7 @@ TEST(ValueTest, CopyConstructor)
         ASSERT_FALSE(v2.isNegativeInfinity());
         ASSERT_FALSE(v2.isNaN());
         ASSERT_FALSE(v2.isNumber());
+        ASSERT_TRUE(v2.isValidNumber());
         ASSERT_TRUE(v2.isBool());
         ASSERT_FALSE(v2.isString());
     }
@@ -339,6 +457,7 @@ TEST(ValueTest, CopyConstructor)
         ASSERT_FALSE(v2.isNegativeInfinity());
         ASSERT_FALSE(v2.isNaN());
         ASSERT_FALSE(v2.isNumber());
+        ASSERT_FALSE(v2.isValidNumber());
         ASSERT_FALSE(v2.isBool());
         ASSERT_TRUE(v2.isString());
     }
@@ -352,6 +471,7 @@ TEST(ValueTest, CopyConstructor)
         ASSERT_FALSE(v2.isNegativeInfinity());
         ASSERT_TRUE(v2.isNaN());
         ASSERT_FALSE(v2.isNumber());
+        ASSERT_FALSE(v2.isValidNumber());
         ASSERT_FALSE(v2.isBool());
         ASSERT_FALSE(v2.isString());
     }
@@ -364,6 +484,7 @@ TEST(ValueTest, CopyConstructor)
         ASSERT_FALSE(v2.isNegativeInfinity());
         ASSERT_FALSE(v2.isNaN());
         ASSERT_FALSE(v2.isNumber());
+        ASSERT_TRUE(v2.isValidNumber());
         ASSERT_FALSE(v2.isBool());
         ASSERT_FALSE(v2.isString());
     }
@@ -376,6 +497,7 @@ TEST(ValueTest, CopyConstructor)
         ASSERT_TRUE(v2.isNegativeInfinity());
         ASSERT_FALSE(v2.isNaN());
         ASSERT_FALSE(v2.isNumber());
+        ASSERT_TRUE(v2.isValidNumber());
         ASSERT_FALSE(v2.isBool());
         ASSERT_FALSE(v2.isString());
     }
@@ -391,6 +513,37 @@ TEST(ValueTest, FloatAssignment)
     ASSERT_FALSE(v.isNegativeInfinity());
     ASSERT_FALSE(v.isNaN());
     ASSERT_TRUE(v.isNumber());
+    ASSERT_TRUE(v.isValidNumber());
+    ASSERT_FALSE(v.isBool());
+    ASSERT_FALSE(v.isString());
+
+    v = std::numeric_limits<float>::infinity();
+    ASSERT_EQ(v.type(), Value::Type::Infinity);
+    ASSERT_TRUE(v.isInfinity());
+    ASSERT_FALSE(v.isNegativeInfinity());
+    ASSERT_FALSE(v.isNaN());
+    ASSERT_FALSE(v.isNumber());
+    ASSERT_TRUE(v.isValidNumber());
+    ASSERT_FALSE(v.isBool());
+    ASSERT_FALSE(v.isString());
+
+    v = -std::numeric_limits<float>::infinity();
+    ASSERT_EQ(v.type(), Value::Type::NegativeInfinity);
+    ASSERT_FALSE(v.isInfinity());
+    ASSERT_TRUE(v.isNegativeInfinity());
+    ASSERT_FALSE(v.isNaN());
+    ASSERT_FALSE(v.isNumber());
+    ASSERT_TRUE(v.isValidNumber());
+    ASSERT_FALSE(v.isBool());
+    ASSERT_FALSE(v.isString());
+
+    v = -std::numeric_limits<float>::quiet_NaN();
+    ASSERT_EQ(v.type(), Value::Type::NaN);
+    ASSERT_FALSE(v.isInfinity());
+    ASSERT_FALSE(v.isNegativeInfinity());
+    ASSERT_TRUE(v.isNaN());
+    ASSERT_FALSE(v.isNumber());
+    ASSERT_FALSE(v.isValidNumber());
     ASSERT_FALSE(v.isBool());
     ASSERT_FALSE(v.isString());
 }
@@ -405,6 +558,37 @@ TEST(ValueTest, DoubleAssignment)
     ASSERT_FALSE(v.isNegativeInfinity());
     ASSERT_FALSE(v.isNaN());
     ASSERT_TRUE(v.isNumber());
+    ASSERT_TRUE(v.isValidNumber());
+    ASSERT_FALSE(v.isBool());
+    ASSERT_FALSE(v.isString());
+
+    v = std::numeric_limits<double>::infinity();
+    ASSERT_EQ(v.type(), Value::Type::Infinity);
+    ASSERT_TRUE(v.isInfinity());
+    ASSERT_FALSE(v.isNegativeInfinity());
+    ASSERT_FALSE(v.isNaN());
+    ASSERT_FALSE(v.isNumber());
+    ASSERT_TRUE(v.isValidNumber());
+    ASSERT_FALSE(v.isBool());
+    ASSERT_FALSE(v.isString());
+
+    v = -std::numeric_limits<double>::infinity();
+    ASSERT_EQ(v.type(), Value::Type::NegativeInfinity);
+    ASSERT_FALSE(v.isInfinity());
+    ASSERT_TRUE(v.isNegativeInfinity());
+    ASSERT_FALSE(v.isNaN());
+    ASSERT_FALSE(v.isNumber());
+    ASSERT_TRUE(v.isValidNumber());
+    ASSERT_FALSE(v.isBool());
+    ASSERT_FALSE(v.isString());
+
+    v = -std::numeric_limits<double>::quiet_NaN();
+    ASSERT_EQ(v.type(), Value::Type::NaN);
+    ASSERT_FALSE(v.isInfinity());
+    ASSERT_FALSE(v.isNegativeInfinity());
+    ASSERT_TRUE(v.isNaN());
+    ASSERT_FALSE(v.isNumber());
+    ASSERT_FALSE(v.isValidNumber());
     ASSERT_FALSE(v.isBool());
     ASSERT_FALSE(v.isString());
 }
@@ -419,6 +603,7 @@ TEST(ValueTest, IntAssignment)
     ASSERT_FALSE(v.isNegativeInfinity());
     ASSERT_FALSE(v.isNaN());
     ASSERT_TRUE(v.isNumber());
+    ASSERT_TRUE(v.isValidNumber());
     ASSERT_FALSE(v.isBool());
     ASSERT_FALSE(v.isString());
 }
@@ -433,6 +618,7 @@ TEST(ValueTest, LongAssignment)
     ASSERT_FALSE(v.isNegativeInfinity());
     ASSERT_FALSE(v.isNaN());
     ASSERT_TRUE(v.isNumber());
+    ASSERT_TRUE(v.isValidNumber());
     ASSERT_FALSE(v.isBool());
     ASSERT_FALSE(v.isString());
 }
@@ -448,6 +634,7 @@ TEST(ValueTest, BoolAssignment)
         ASSERT_FALSE(v.isNegativeInfinity());
         ASSERT_FALSE(v.isNaN());
         ASSERT_FALSE(v.isNumber());
+        ASSERT_TRUE(v.isValidNumber());
         ASSERT_TRUE(v.isBool());
         ASSERT_FALSE(v.isString());
     }
@@ -461,6 +648,7 @@ TEST(ValueTest, BoolAssignment)
         ASSERT_FALSE(v.isNegativeInfinity());
         ASSERT_FALSE(v.isNaN());
         ASSERT_FALSE(v.isNumber());
+        ASSERT_TRUE(v.isValidNumber());
         ASSERT_TRUE(v.isBool());
         ASSERT_FALSE(v.isString());
     }
@@ -477,6 +665,7 @@ TEST(ValueTest, StdStringAssignment)
         ASSERT_FALSE(v.isNegativeInfinity());
         ASSERT_FALSE(v.isNaN());
         ASSERT_FALSE(v.isNumber());
+        ASSERT_FALSE(v.isValidNumber());
         ASSERT_FALSE(v.isBool());
         ASSERT_TRUE(v.isString());
     }
@@ -490,6 +679,7 @@ TEST(ValueTest, StdStringAssignment)
         ASSERT_FALSE(v.isNegativeInfinity());
         ASSERT_FALSE(v.isNaN());
         ASSERT_FALSE(v.isNumber());
+        ASSERT_TRUE(v.isValidNumber());
         ASSERT_FALSE(v.isBool());
         ASSERT_FALSE(v.isString());
     }
@@ -503,6 +693,7 @@ TEST(ValueTest, StdStringAssignment)
         ASSERT_TRUE(v.isNegativeInfinity());
         ASSERT_FALSE(v.isNaN());
         ASSERT_FALSE(v.isNumber());
+        ASSERT_TRUE(v.isValidNumber());
         ASSERT_FALSE(v.isBool());
         ASSERT_FALSE(v.isString());
     }
@@ -516,6 +707,7 @@ TEST(ValueTest, StdStringAssignment)
         ASSERT_FALSE(v.isNegativeInfinity());
         ASSERT_TRUE(v.isNaN());
         ASSERT_FALSE(v.isNumber());
+        ASSERT_FALSE(v.isValidNumber());
         ASSERT_FALSE(v.isBool());
         ASSERT_FALSE(v.isString());
     }
@@ -532,6 +724,7 @@ TEST(ValueTest, CStringAssignment)
         ASSERT_FALSE(v.isNegativeInfinity());
         ASSERT_FALSE(v.isNaN());
         ASSERT_FALSE(v.isNumber());
+        ASSERT_FALSE(v.isValidNumber());
         ASSERT_FALSE(v.isBool());
         ASSERT_TRUE(v.isString());
     }
@@ -545,6 +738,7 @@ TEST(ValueTest, CStringAssignment)
         ASSERT_FALSE(v.isNegativeInfinity());
         ASSERT_FALSE(v.isNaN());
         ASSERT_FALSE(v.isNumber());
+        ASSERT_TRUE(v.isValidNumber());
         ASSERT_FALSE(v.isBool());
         ASSERT_FALSE(v.isString());
     }
@@ -558,6 +752,7 @@ TEST(ValueTest, CStringAssignment)
         ASSERT_TRUE(v.isNegativeInfinity());
         ASSERT_FALSE(v.isNaN());
         ASSERT_FALSE(v.isNumber());
+        ASSERT_TRUE(v.isValidNumber());
         ASSERT_FALSE(v.isBool());
         ASSERT_FALSE(v.isString());
     }
@@ -571,6 +766,7 @@ TEST(ValueTest, CStringAssignment)
         ASSERT_FALSE(v.isNegativeInfinity());
         ASSERT_TRUE(v.isNaN());
         ASSERT_FALSE(v.isNumber());
+        ASSERT_FALSE(v.isValidNumber());
         ASSERT_FALSE(v.isBool());
         ASSERT_FALSE(v.isString());
     }
@@ -585,6 +781,7 @@ TEST(ValueTest, InfinityAssignment)
     ASSERT_FALSE(v.isNegativeInfinity());
     ASSERT_FALSE(v.isNaN());
     ASSERT_FALSE(v.isNumber());
+    ASSERT_TRUE(v.isValidNumber());
     ASSERT_FALSE(v.isBool());
     ASSERT_FALSE(v.isString());
 }
@@ -598,6 +795,7 @@ TEST(ValueTest, NegativeInfinityAssignment)
     ASSERT_TRUE(v.isNegativeInfinity());
     ASSERT_FALSE(v.isNaN());
     ASSERT_FALSE(v.isNumber());
+    ASSERT_TRUE(v.isValidNumber());
     ASSERT_FALSE(v.isBool());
     ASSERT_FALSE(v.isString());
 }
@@ -611,6 +809,7 @@ TEST(ValueTest, NaNAssignment)
     ASSERT_FALSE(v.isNegativeInfinity());
     ASSERT_TRUE(v.isNaN());
     ASSERT_FALSE(v.isNumber());
+    ASSERT_FALSE(v.isValidNumber());
     ASSERT_FALSE(v.isBool());
     ASSERT_FALSE(v.isString());
 }
@@ -627,6 +826,7 @@ TEST(ValueTest, CopyAssignment)
         ASSERT_FALSE(v2.isNegativeInfinity());
         ASSERT_FALSE(v2.isNaN());
         ASSERT_TRUE(v2.isNumber());
+        ASSERT_TRUE(v2.isValidNumber());
         ASSERT_FALSE(v2.isBool());
         ASSERT_FALSE(v2.isString());
     }
@@ -641,6 +841,7 @@ TEST(ValueTest, CopyAssignment)
         ASSERT_FALSE(v2.isNegativeInfinity());
         ASSERT_FALSE(v2.isNaN());
         ASSERT_TRUE(v2.isNumber());
+        ASSERT_TRUE(v2.isValidNumber());
         ASSERT_FALSE(v2.isBool());
         ASSERT_FALSE(v2.isString());
     }
@@ -655,6 +856,7 @@ TEST(ValueTest, CopyAssignment)
         ASSERT_FALSE(v2.isNegativeInfinity());
         ASSERT_FALSE(v2.isNaN());
         ASSERT_FALSE(v2.isNumber());
+        ASSERT_TRUE(v2.isValidNumber());
         ASSERT_TRUE(v2.isBool());
         ASSERT_FALSE(v2.isString());
     }
@@ -669,6 +871,7 @@ TEST(ValueTest, CopyAssignment)
         ASSERT_FALSE(v2.isNegativeInfinity());
         ASSERT_FALSE(v2.isNaN());
         ASSERT_FALSE(v2.isNumber());
+        ASSERT_FALSE(v2.isValidNumber());
         ASSERT_FALSE(v2.isBool());
         ASSERT_TRUE(v2.isString());
     }
@@ -683,6 +886,7 @@ TEST(ValueTest, CopyAssignment)
         ASSERT_FALSE(v2.isNegativeInfinity());
         ASSERT_TRUE(v2.isNaN());
         ASSERT_FALSE(v2.isNumber());
+        ASSERT_FALSE(v2.isValidNumber());
         ASSERT_FALSE(v2.isBool());
         ASSERT_FALSE(v2.isString());
     }
@@ -696,6 +900,7 @@ TEST(ValueTest, CopyAssignment)
         ASSERT_FALSE(v2.isNegativeInfinity());
         ASSERT_FALSE(v2.isNaN());
         ASSERT_FALSE(v2.isNumber());
+        ASSERT_TRUE(v2.isValidNumber());
         ASSERT_FALSE(v2.isBool());
         ASSERT_FALSE(v2.isString());
     }
@@ -709,6 +914,7 @@ TEST(ValueTest, CopyAssignment)
         ASSERT_TRUE(v2.isNegativeInfinity());
         ASSERT_FALSE(v2.isNaN());
         ASSERT_FALSE(v2.isNumber());
+        ASSERT_TRUE(v2.isValidNumber());
         ASSERT_FALSE(v2.isBool());
         ASSERT_FALSE(v2.isString());
     }
@@ -752,9 +958,9 @@ TEST(ValueTest, ToInt)
     ASSERT_EQ(v.toInt(), 0); // booleans represented as string shouldn't convert
 
     v = "Infinity";
-    ASSERT_EQ(v.toInt(), std::numeric_limits<int>::infinity());
+    ASSERT_EQ(v.toInt(), 0);
     v = "-Infinity";
-    ASSERT_EQ(v.toInt(), -std::numeric_limits<int>::infinity());
+    ASSERT_EQ(v.toInt(), 0);
     v = "NaN";
     ASSERT_EQ(v.toInt(), 0);
 
@@ -855,9 +1061,9 @@ TEST(ValueTest, ToLong)
     ASSERT_EQ(v.toLong(), 0); // booleans represented as string shouldn't convert
 
     v = "Infinity";
-    ASSERT_EQ(v.toLong(), std::numeric_limits<long>::infinity());
+    ASSERT_EQ(v.toLong(), 0);
     v = "-Infinity";
-    ASSERT_EQ(v.toLong(), -std::numeric_limits<long>::infinity());
+    ASSERT_EQ(v.toLong(), 0);
     v = "NaN";
     ASSERT_EQ(v.toLong(), 0);
 
@@ -968,9 +1174,9 @@ TEST(ValueTest, ToDouble)
     ASSERT_EQ(v.toDouble(), 0.0); // booleans represented as string shouldn't convert
 
     v = "Infinity";
-    ASSERT_EQ(v.toDouble(), std::numeric_limits<double>::infinity());
+    ASSERT_TRUE(v.toDouble() > 0 && std::isinf(v.toDouble()));
     v = "-Infinity";
-    ASSERT_EQ(v.toDouble(), -std::numeric_limits<double>::infinity());
+    ASSERT_TRUE(v.toDouble() < 0 && std::isinf(v.toDouble()));
     v = "NaN";
     ASSERT_EQ(v.toDouble(), 0.0);
 
@@ -1208,6 +1414,22 @@ TEST(ValueTest, ToString)
     ASSERT_EQ(v.toString(), "-2550.625");
     ASSERT_EQ(utf8::utf16to8(v.toUtf16()), v.toString());
 
+    v = 9.4324e+20;
+    ASSERT_EQ(v.toString(), "943240000000000000000");
+    ASSERT_EQ(utf8::utf16to8(v.toUtf16()), v.toString());
+
+    v = -2.591e-2;
+    ASSERT_EQ(v.toString(), "-0.02591");
+    ASSERT_EQ(utf8::utf16to8(v.toUtf16()), v.toString());
+
+    v = 9.4324e+21;
+    ASSERT_EQ(v.toString(), "9.4324e+21");
+    ASSERT_EQ(utf8::utf16to8(v.toUtf16()), v.toString());
+
+    v = -2.591e-13;
+    ASSERT_EQ(v.toString(), "-2.591e-13");
+    ASSERT_EQ(utf8::utf16to8(v.toUtf16()), v.toString());
+
     v = 0.001;
     ASSERT_EQ(v.toString(), "0.001");
     ASSERT_EQ(utf8::utf16to8(v.toUtf16()), v.toString());
@@ -1244,17 +1466,17 @@ TEST(ValueTest, ToString)
     ASSERT_EQ(utf8::utf16to8(v.toUtf16()), v.toString());
 
     v = "9432.4e-12";
-    ASSERT_EQ(v.toString(), "9.4324e-9");
+    ASSERT_EQ(v.toString(), "9432.4e-12");
     ASSERT_EQ(utf8::utf16to8(v.toUtf16()), v.toString());
     v = "-9432.4e-12";
-    ASSERT_EQ(v.toString(), "-9.4324e-9");
+    ASSERT_EQ(v.toString(), "-9432.4e-12");
     ASSERT_EQ(utf8::utf16to8(v.toUtf16()), v.toString());
 
     v = "9432.4e+6";
-    ASSERT_EQ(v.toString(), "9432400000");
+    ASSERT_EQ(v.toString(), "9432.4e+6");
     ASSERT_EQ(utf8::utf16to8(v.toUtf16()), v.toString());
     v = "-9432.4e+6";
-    ASSERT_EQ(v.toString(), "-9432400000");
+    ASSERT_EQ(v.toString(), "-9432.4e+6");
     ASSERT_EQ(utf8::utf16to8(v.toUtf16()), v.toString());
 
     v = "false";

From d3add54ae65ac32b30c4b80bddbd37d711c23f23 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Sun, 24 Mar 2024 18:09:07 +0100
Subject: [PATCH 50/77] Add more Value tests

---
 test/scratch_classes/value_test.cpp | 57 +++++++++++++++++++++++++++++
 1 file changed, 57 insertions(+)

diff --git a/test/scratch_classes/value_test.cpp b/test/scratch_classes/value_test.cpp
index 4ac41c09..050dccea 100644
--- a/test/scratch_classes/value_test.cpp
+++ b/test/scratch_classes/value_test.cpp
@@ -2411,6 +2411,63 @@ TEST(ValueTest, EqualityOperators)
         ASSERT_FALSE(v2 != v6);
     }
 
+    {
+        Value v1 = true;
+        Value v2 = false;
+        Value v3 = "00001";
+        Value v4 = "00000";
+
+        ASSERT_TRUE(v1 == v3);
+        ASSERT_FALSE(v1 != v3);
+
+        ASSERT_FALSE(v1 == v4);
+        ASSERT_TRUE(v1 != v4);
+
+        ASSERT_TRUE(v2 == v4);
+        ASSERT_FALSE(v2 != v4);
+
+        ASSERT_FALSE(v2 == v3);
+        ASSERT_TRUE(v2 != v3);
+    }
+
+    {
+        Value v1 = "true";
+        Value v2 = "false";
+        Value v3 = 1;
+        Value v4 = 0;
+
+        ASSERT_FALSE(v1 == v3);
+        ASSERT_TRUE(v1 != v3);
+
+        ASSERT_FALSE(v1 == v4);
+        ASSERT_TRUE(v1 != v4);
+
+        ASSERT_FALSE(v2 == v4);
+        ASSERT_TRUE(v2 != v4);
+
+        ASSERT_FALSE(v2 == v3);
+        ASSERT_TRUE(v2 != v3);
+    }
+
+    {
+        Value v1 = "true";
+        Value v2 = "false";
+        Value v3 = "TRUE";
+        Value v4 = "FALSE";
+
+        ASSERT_TRUE(v1 == v3);
+        ASSERT_FALSE(v1 != v3);
+
+        ASSERT_FALSE(v1 == v4);
+        ASSERT_TRUE(v1 != v4);
+
+        ASSERT_TRUE(v2 == v4);
+        ASSERT_FALSE(v2 != v4);
+
+        ASSERT_FALSE(v2 == v3);
+        ASSERT_TRUE(v2 != v3);
+    }
+
     {
         Value v1 = true;
         Value v2 = false;

From af4522a635ab27cdab1f40d62129e8584be37f76 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Sun, 24 Mar 2024 18:33:07 +0100
Subject: [PATCH 51/77] Add isInt() method to Value

---
 include/scratchcpp/value.h          | 23 +++++++++++++++++++++
 test/scratch_classes/value_test.cpp | 32 +++++++++++++++++++++++++++++
 2 files changed, 55 insertions(+)

diff --git a/include/scratchcpp/value.h b/include/scratchcpp/value.h
index 3a6091a3..ee9aab9c 100644
--- a/include/scratchcpp/value.h
+++ b/include/scratchcpp/value.h
@@ -236,6 +236,29 @@ class LIBSCRATCHCPP_EXPORT Value
             }
         }
 
+        /*! Returns true if this value represents a round integer. */
+        bool isInt() const
+        {
+            // https://github.com/scratchfoundation/scratch-vm/blob/112989da0e7306eeb405a5c52616e41c2164af24/src/util/cast.js#L157-L181
+            switch (m_type) {
+                case Type::Integer:
+                case Type::Bool:
+                case Type::Infinity:
+                case Type::NegativeInfinity:
+                case Type::NaN:
+                    return true;
+                case Type::Double: {
+                    double intpart;
+                    std::modf(m_doubleValue, &intpart);
+                    return m_doubleValue == intpart;
+                }
+                case Type::String:
+                    return m_stringValue.find('.') == std::string::npos;
+            }
+
+            return false;
+        }
+
         /*! Returns true if the value is a boolean. */
         bool isBool() const { return m_type == Type::Bool; }
 
diff --git a/test/scratch_classes/value_test.cpp b/test/scratch_classes/value_test.cpp
index 050dccea..9c6763c4 100644
--- a/test/scratch_classes/value_test.cpp
+++ b/test/scratch_classes/value_test.cpp
@@ -14,6 +14,7 @@ TEST(ValueTest, DefaultConstructor)
     ASSERT_FALSE(v.isNaN());
     ASSERT_TRUE(v.isNumber());
     ASSERT_TRUE(v.isValidNumber());
+    ASSERT_TRUE(v.isInt());
     ASSERT_FALSE(v.isBool());
     ASSERT_FALSE(v.isString());
 }
@@ -29,6 +30,7 @@ TEST(ValueTest, FloatConstructor)
         ASSERT_FALSE(v.isNaN());
         ASSERT_TRUE(v.isNumber());
         ASSERT_TRUE(v.isValidNumber());
+        ASSERT_FALSE(v.isInt());
         ASSERT_FALSE(v.isBool());
         ASSERT_FALSE(v.isString());
     }
@@ -81,6 +83,21 @@ TEST(ValueTest, DoubleConstructor)
         ASSERT_FALSE(v.isNaN());
         ASSERT_TRUE(v.isNumber());
         ASSERT_TRUE(v.isValidNumber());
+        ASSERT_FALSE(v.isInt());
+        ASSERT_FALSE(v.isBool());
+        ASSERT_FALSE(v.isString());
+    }
+
+    {
+        Value v(static_cast<double>(-5.0));
+        ASSERT_EQ(v.toDouble(), -5.0);
+        ASSERT_EQ(v.type(), Value::Type::Double);
+        ASSERT_FALSE(v.isInfinity());
+        ASSERT_FALSE(v.isNegativeInfinity());
+        ASSERT_FALSE(v.isNaN());
+        ASSERT_TRUE(v.isNumber());
+        ASSERT_TRUE(v.isValidNumber());
+        ASSERT_TRUE(v.isInt());
         ASSERT_FALSE(v.isBool());
         ASSERT_FALSE(v.isString());
     }
@@ -132,6 +149,7 @@ TEST(ValueTest, IntConstructor)
     ASSERT_FALSE(v.isNaN());
     ASSERT_TRUE(v.isNumber());
     ASSERT_TRUE(v.isValidNumber());
+    ASSERT_TRUE(v.isInt());
     ASSERT_FALSE(v.isBool());
     ASSERT_FALSE(v.isString());
 }
@@ -146,6 +164,7 @@ TEST(ValueTest, SizeTConstructor)
     ASSERT_FALSE(v.isNaN());
     ASSERT_TRUE(v.isNumber());
     ASSERT_TRUE(v.isValidNumber());
+    ASSERT_TRUE(v.isInt());
     ASSERT_FALSE(v.isBool());
     ASSERT_FALSE(v.isString());
 }
@@ -160,6 +179,7 @@ TEST(ValueTest, LongConstructor)
     ASSERT_FALSE(v.isNaN());
     ASSERT_TRUE(v.isNumber());
     ASSERT_TRUE(v.isValidNumber());
+    ASSERT_TRUE(v.isInt());
     ASSERT_FALSE(v.isBool());
     ASSERT_FALSE(v.isString());
 }
@@ -175,6 +195,7 @@ TEST(ValueTest, BoolConstructor)
         ASSERT_FALSE(v.isNaN());
         ASSERT_FALSE(v.isNumber());
         ASSERT_TRUE(v.isValidNumber());
+        ASSERT_TRUE(v.isInt());
         ASSERT_TRUE(v.isBool());
         ASSERT_FALSE(v.isString());
     }
@@ -188,6 +209,7 @@ TEST(ValueTest, BoolConstructor)
         ASSERT_FALSE(v.isNaN());
         ASSERT_FALSE(v.isNumber());
         ASSERT_TRUE(v.isValidNumber());
+        ASSERT_TRUE(v.isInt());
         ASSERT_TRUE(v.isBool());
         ASSERT_FALSE(v.isString());
     }
@@ -204,6 +226,7 @@ TEST(ValueTest, StdStringConstructor)
         ASSERT_FALSE(v.isNaN());
         ASSERT_FALSE(v.isNumber());
         ASSERT_FALSE(v.isValidNumber());
+        ASSERT_TRUE(v.isInt());
         ASSERT_FALSE(v.isBool());
         ASSERT_TRUE(v.isString());
     }
@@ -217,6 +240,7 @@ TEST(ValueTest, StdStringConstructor)
         ASSERT_FALSE(v.isNaN());
         ASSERT_FALSE(v.isNumber());
         ASSERT_TRUE(v.isValidNumber());
+        ASSERT_TRUE(v.isInt());
         ASSERT_FALSE(v.isBool());
         ASSERT_TRUE(v.isString());
     }
@@ -231,6 +255,7 @@ TEST(ValueTest, StdStringConstructor)
         ASSERT_FALSE(v.isNaN());
         ASSERT_FALSE(v.isNumber());
         ASSERT_TRUE(v.isValidNumber());
+        ASSERT_FALSE(v.isInt());
         ASSERT_FALSE(v.isBool());
         ASSERT_TRUE(v.isString());
     }
@@ -244,6 +269,7 @@ TEST(ValueTest, StdStringConstructor)
         ASSERT_FALSE(v.isNaN());
         ASSERT_FALSE(v.isNumber());
         ASSERT_FALSE(v.isValidNumber());
+        ASSERT_TRUE(v.isInt());
         ASSERT_FALSE(v.isBool());
         ASSERT_TRUE(v.isString());
     }
@@ -258,6 +284,7 @@ TEST(ValueTest, StdStringConstructor)
         ASSERT_FALSE(v.isNaN());
         ASSERT_FALSE(v.isNumber());
         ASSERT_TRUE(v.isValidNumber());
+        ASSERT_TRUE(v.isInt());
         ASSERT_FALSE(v.isBool());
         ASSERT_TRUE(v.isString());
     }
@@ -272,6 +299,7 @@ TEST(ValueTest, StdStringConstructor)
         ASSERT_FALSE(v.isNaN());
         ASSERT_FALSE(v.isNumber());
         ASSERT_FALSE(v.isValidNumber());
+        ASSERT_TRUE(v.isInt());
         ASSERT_FALSE(v.isBool());
         ASSERT_TRUE(v.isString());
     }
@@ -324,6 +352,7 @@ TEST(ValueTest, CStringConstructor)
         ASSERT_FALSE(v.isNaN());
         ASSERT_FALSE(v.isNumber());
         ASSERT_FALSE(v.isValidNumber());
+        ASSERT_TRUE(v.isInt());
         ASSERT_FALSE(v.isBool());
         ASSERT_TRUE(v.isString());
     }
@@ -374,6 +403,7 @@ TEST(ValueTest, InfinityConstructor)
     ASSERT_FALSE(v.isNaN());
     ASSERT_FALSE(v.isNumber());
     ASSERT_TRUE(v.isValidNumber());
+    ASSERT_TRUE(v.isInt());
     ASSERT_FALSE(v.isBool());
     ASSERT_FALSE(v.isString());
 }
@@ -387,6 +417,7 @@ TEST(ValueTest, NegativeInfinityConstructor)
     ASSERT_FALSE(v.isNaN());
     ASSERT_FALSE(v.isNumber());
     ASSERT_TRUE(v.isValidNumber());
+    ASSERT_TRUE(v.isInt());
     ASSERT_FALSE(v.isBool());
     ASSERT_FALSE(v.isString());
 }
@@ -400,6 +431,7 @@ TEST(ValueTest, NaNConstructor)
     ASSERT_TRUE(v.isNaN());
     ASSERT_FALSE(v.isNumber());
     ASSERT_FALSE(v.isValidNumber());
+    ASSERT_TRUE(v.isInt());
     ASSERT_FALSE(v.isBool());
     ASSERT_FALSE(v.isString());
 }

From 3b6d6584dc0a18dc8ae898b3e88838c6b3ac627f Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Sun, 24 Mar 2024 18:33:26 +0100
Subject: [PATCH 52/77] Fix type casting in VirtualMachine

---
 src/engine/virtualmachine_p.cpp               | 34 ++++++-------
 test/virtual_machine/virtual_machine_test.cpp | 50 +++++++++++++++++--
 2 files changed, 62 insertions(+), 22 deletions(-)

diff --git a/src/engine/virtualmachine_p.cpp b/src/engine/virtualmachine_p.cpp
index cdbc5c9a..b9a99521 100644
--- a/src/engine/virtualmachine_p.cpp
+++ b/src/engine/virtualmachine_p.cpp
@@ -438,7 +438,7 @@ unsigned int *VirtualMachinePrivate::run(unsigned int *pos, bool reset)
     DISPATCH();
 
     OP(RANDOM) :
-        if ((READ_REG(0, 2)->type() == Value::Type::Integer) && (READ_REG(1, 2)->type() == Value::Type::Integer)) REPLACE_RET_VALUE(rng->randint(READ_REG(0, 2)->toInt(), READ_REG(1, 2)->toInt()), 2);
+        if (READ_REG(0, 2)->isInt() && READ_REG(1, 2)->isInt()) REPLACE_RET_VALUE(rng->randint(READ_REG(0, 2)->toInt(), READ_REG(1, 2)->toInt()), 2);
     else REPLACE_RET_VALUE(rng->randintDouble(READ_REG(0, 2)->toDouble(), READ_REG(1, 2)->toDouble()), 2);
     FREE_REGS(1);
     DISPATCH();
@@ -621,7 +621,10 @@ unsigned int *VirtualMachinePrivate::run(unsigned int *pos, bool reset)
         const Value *indexValue = READ_LAST_REG();
         size_t index;
         List *list = lists[*++pos];
-        if (indexValue->isString()) {
+        if (indexValue->isValidNumber()) {
+            index = indexValue->toLong();
+            FIX_LIST_INDEX(index, list->size());
+        } else {
             const std::string &str = indexValue->toString();
             if (str == "last") {
                 index = list->size();
@@ -633,9 +636,6 @@ unsigned int *VirtualMachinePrivate::run(unsigned int *pos, bool reset)
                 index = size == 0 ? 0 : rng->randint(1, size);
             } else
                 index = 0;
-        } else {
-            index = indexValue->toLong();
-            FIX_LIST_INDEX(index, list->size());
         }
         if (index != 0)
             list->removeAt(index - 1);
@@ -652,7 +652,10 @@ unsigned int *VirtualMachinePrivate::run(unsigned int *pos, bool reset)
         const Value *indexValue = READ_REG(1, 2);
         size_t index;
         List *list = lists[*++pos];
-        if (indexValue->isString()) {
+        if (indexValue->isValidNumber()) {
+            index = indexValue->toLong();
+            FIX_LIST_INDEX(index, list->size());
+        } else {
             const std::string &str = indexValue->toString();
             if (str == "last") {
                 list->push_back(*READ_REG(0, 2));
@@ -662,9 +665,6 @@ unsigned int *VirtualMachinePrivate::run(unsigned int *pos, bool reset)
                 index = size == 0 ? 1 : rng->randint(1, size);
             } else
                 index = 0;
-        } else {
-            index = indexValue->toLong();
-            FIX_LIST_INDEX(index, list->size());
         }
         if ((index != 0) || list->empty()) {
             if (list->empty())
@@ -681,7 +681,10 @@ unsigned int *VirtualMachinePrivate::run(unsigned int *pos, bool reset)
         const Value *indexValue = READ_REG(0, 2);
         size_t index;
         List *list = lists[*++pos];
-        if (indexValue->isString()) {
+        if (indexValue->isValidNumber()) {
+            index = indexValue->toLong();
+            FIX_LIST_INDEX(index, list->size());
+        } else {
             std::string str = indexValue->toString();
             if (str == "last")
                 index = list->size();
@@ -690,9 +693,6 @@ unsigned int *VirtualMachinePrivate::run(unsigned int *pos, bool reset)
                 index = size == 0 ? 0 : rng->randint(1, size);
             } else
                 index = 0;
-        } else {
-            index = indexValue->toLong();
-            FIX_LIST_INDEX(index, list->size());
         }
         if (index != 0)
             list->operator[](index - 1) = *READ_REG(1, 2);
@@ -705,7 +705,10 @@ unsigned int *VirtualMachinePrivate::run(unsigned int *pos, bool reset)
         const Value *indexValue = READ_LAST_REG();
         size_t index;
         List *list = lists[*++pos];
-        if (indexValue->isString()) {
+        if (indexValue->isValidNumber()) {
+            index = indexValue->toLong();
+            FIX_LIST_INDEX(index, list->size());
+        } else {
             std::string str = indexValue->toString();
             if (str == "last")
                 index = list->size();
@@ -714,9 +717,6 @@ unsigned int *VirtualMachinePrivate::run(unsigned int *pos, bool reset)
                 index = size == 0 ? 0 : rng->randint(1, size);
             } else
                 index = 0;
-        } else {
-            index = indexValue->toLong();
-            FIX_LIST_INDEX(index, list->size());
         }
         if (index == 0) {
             REPLACE_RET_VALUE("", 1);
diff --git a/test/virtual_machine/virtual_machine_test.cpp b/test/virtual_machine/virtual_machine_test.cpp
index 9308116e..30ea6dbb 100644
--- a/test/virtual_machine/virtual_machine_test.cpp
+++ b/test/virtual_machine/virtual_machine_test.cpp
@@ -412,7 +412,11 @@ TEST(VirtualMachineTest, OP_RANDOM)
     static unsigned int bytecode2[] = { OP_START, OP_CONST, 1, OP_CONST, 2, OP_RANDOM, OP_HALT };
     static unsigned int bytecode3[] = { OP_START, OP_CONST, 3, OP_CONST, 0, OP_RANDOM, OP_HALT };
     static unsigned int bytecode4[] = { OP_START, OP_CONST, 2, OP_CONST, 3, OP_RANDOM, OP_HALT };
-    static Value constValues[] = { -45, 12, 6.05, -78.686 };
+    static unsigned int bytecode5[] = { OP_START, OP_CONST, 4, OP_CONST, 5, OP_RANDOM, OP_HALT };
+    static unsigned int bytecode6[] = { OP_START, OP_CONST, 5, OP_CONST, 6, OP_RANDOM, OP_HALT };
+    static unsigned int bytecode7[] = { OP_START, OP_CONST, 7, OP_CONST, 4, OP_RANDOM, OP_HALT };
+    static unsigned int bytecode8[] = { OP_START, OP_CONST, 6, OP_CONST, 7, OP_RANDOM, OP_HALT };
+    static Value constValues[] = { -45, 12, 6.05, -78.686, "-45", "12", "6.05", "-78.686" };
 
     VirtualMachinePrivate vm(nullptr, nullptr, nullptr, nullptr);
     vm.constValues = constValues;
@@ -455,6 +459,42 @@ TEST(VirtualMachineTest, OP_RANDOM)
 
     ASSERT_EQ(vm.regCount, 1);
     ASSERT_EQ(vm.regs[vm.regCount - 1]->toDouble(), -28.648764);
+
+    EXPECT_CALL(rng, randint(-45, 12)).WillOnce(Return(-18));
+    vm.bytecode = bytecode5;
+    vm.pos = bytecode5;
+    vm.regCount = 0;
+    vm.run(vm.pos);
+
+    ASSERT_EQ(vm.regCount, 1);
+    ASSERT_EQ(vm.regs[vm.regCount - 1]->toDouble(), -18);
+
+    EXPECT_CALL(rng, randintDouble(12, 6.05)).WillOnce(Return(3.486789));
+    vm.bytecode = bytecode6;
+    vm.pos = bytecode6;
+    vm.regCount = 0;
+    vm.run(vm.pos);
+
+    ASSERT_EQ(vm.regCount, 1);
+    ASSERT_EQ(vm.regs[vm.regCount - 1]->toDouble(), 3.486789);
+
+    EXPECT_CALL(rng, randintDouble(-78.686, -45)).WillOnce(Return(-59.468873));
+    vm.bytecode = bytecode7;
+    vm.pos = bytecode7;
+    vm.regCount = 0;
+    vm.run(vm.pos);
+
+    ASSERT_EQ(vm.regCount, 1);
+    ASSERT_EQ(vm.regs[vm.regCount - 1]->toDouble(), -59.468873);
+
+    EXPECT_CALL(rng, randintDouble(6.05, -78.686)).WillOnce(Return(-28.648764));
+    vm.bytecode = bytecode8;
+    vm.pos = bytecode8;
+    vm.regCount = 0;
+    vm.run(vm.pos);
+
+    ASSERT_EQ(vm.regCount, 1);
+    ASSERT_EQ(vm.regs[vm.regCount - 1]->toDouble(), -28.648764);
 }
 
 TEST(VirtualMachineTest, OP_ROUND)
@@ -955,7 +995,7 @@ TEST(VirtualMachineTest, OP_LIST_DEL)
         0,        OP_CONST, 9,  OP_LIST_DEL, 0, OP_READ_LIST, 0, OP_CONST, 10, OP_LIST_DEL, 0, OP_READ_LIST, 0, OP_CONST, 11, OP_LIST_DEL, 0, OP_READ_LIST,
         0,        OP_CONST, 12, OP_LIST_DEL, 0, OP_READ_LIST, 0, OP_CONST, 13, OP_LIST_DEL, 0, OP_READ_LIST, 0, OP_HALT
     };
-    static Value constValues[] = { 3, 1, 6, 0, 7, -1, 9, Value::SpecialValue::NegativeInfinity, Value::SpecialValue::Infinity, Value::SpecialValue::NaN, "invalid", "last", "random", "all" };
+    static Value constValues[] = { 3, 1, "6", 0, 7, -1, 9, Value::SpecialValue::NegativeInfinity, Value::SpecialValue::Infinity, Value::SpecialValue::NaN, "invalid", "last", "random", "all" };
     List list1("", "list1");
     list1.push_back("a");
     list1.push_back("b");
@@ -1027,7 +1067,7 @@ TEST(VirtualMachineTest, OP_LIST_INSERT)
         0,        OP_CONST,       0,  OP_CONST,       10, OP_LIST_INSERT, 0, OP_READ_LIST,   0, OP_CONST,       0,  OP_CONST,       11, OP_LIST_INSERT, 0, OP_READ_LIST,   0, OP_CONST,
         0,        OP_CONST,       12, OP_LIST_INSERT, 0,  OP_READ_LIST,   0, OP_CONST,       0, OP_CONST,       13, OP_LIST_INSERT, 0,  OP_READ_LIST,   0, OP_HALT
     };
-    static Value constValues[] = { "new item", 3, 1, 10, 0, 12, -1, 14, Value::SpecialValue::NegativeInfinity, Value::SpecialValue::Infinity, Value::SpecialValue::NaN, "last", "random", "invalid" };
+    static Value constValues[] = { "new item", "3", 1, 10, 0, 12, -1, 14, Value::SpecialValue::NegativeInfinity, Value::SpecialValue::Infinity, Value::SpecialValue::NaN, "last", "random", "invalid" };
     List list1("", "list1");
     list1.push_back("a");
     list1.push_back("b");
@@ -1079,7 +1119,7 @@ TEST(VirtualMachineTest, OP_LIST_REPLACE)
         12,       OP_CONST,        14, OP_LIST_REPLACE, 0, OP_READ_LIST,    0, OP_CONST,        13, OP_CONST,        0,  OP_LIST_REPLACE, 0,  OP_READ_LIST,    0, OP_HALT
     };
     static Value constValues[] = {
-        "new item", 3, 1, 8, 0, 9, -1, 12, Value::SpecialValue::NegativeInfinity, Value::SpecialValue::Infinity, Value::SpecialValue::NaN, "last", "random", "invalid", "test"
+        "new item", 3, "1", 8, 0, 9, -1, 12, Value::SpecialValue::NegativeInfinity, Value::SpecialValue::Infinity, Value::SpecialValue::NaN, "last", "random", "invalid", "test"
     };
     List list1("", "list1");
     list1.push_back("a");
@@ -1128,7 +1168,7 @@ TEST(VirtualMachineTest, OP_LIST_GET_ITEM)
         0,        OP_CONST, 5,  OP_LIST_GET_ITEM, 0, OP_CONST, 6,  OP_LIST_GET_ITEM, 0, OP_CONST, 7,  OP_LIST_GET_ITEM, 0, OP_CONST, 8, OP_LIST_GET_ITEM, 0, OP_CONST, 9, OP_LIST_GET_ITEM,
         0,        OP_CONST, 10, OP_LIST_GET_ITEM, 0, OP_CONST, 11, OP_LIST_GET_ITEM, 0, OP_CONST, 12, OP_LIST_GET_ITEM, 0, OP_HALT
     };
-    static Value constValues[] = { 3, 1, 8, 0, 9, -1, 12, Value::SpecialValue::NegativeInfinity, Value::SpecialValue::Infinity, Value::SpecialValue::NaN, "last", "random", "invalid" };
+    static Value constValues[] = { 3, 1, "8", 0, 9, -1, 12, Value::SpecialValue::NegativeInfinity, Value::SpecialValue::Infinity, Value::SpecialValue::NaN, "last", "random", "invalid" };
     List list1("", "list1");
     list1.push_back("a");
     list1.push_back("b");

From f1f5e6182a4f245a10979f12505c2edcf5e2cdf4 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Sun, 24 Mar 2024 18:55:24 +0100
Subject: [PATCH 53/77] Fix crashes when removing bubble and sound info

---
 src/blocks/looksblocks.cpp | 20 +++++++++++++-------
 src/blocks/soundblocks.cpp | 22 +++++++++++++---------
 2 files changed, 26 insertions(+), 16 deletions(-)

diff --git a/src/blocks/looksblocks.cpp b/src/blocks/looksblocks.cpp
index 4c98e26b..b10691a4 100644
--- a/src/blocks/looksblocks.cpp
+++ b/src/blocks/looksblocks.cpp
@@ -19,6 +19,18 @@ using namespace libscratchcpp;
 IRandomGenerator *LooksBlocks::rng = nullptr;
 IClock *LooksBlocks::clock = nullptr;
 
+// TODO: Use C++20
+template<typename ContainerT, typename PredicateT>
+void erase_if(ContainerT &items, const PredicateT &predicate)
+{
+    for (auto it = items.begin(); it != items.end();) {
+        if (predicate(*it))
+            it = items.erase(it);
+        else
+            ++it;
+    }
+}
+
 std::string LooksBlocks::name() const
 {
     return "Looks";
@@ -89,13 +101,7 @@ void LooksBlocks::onInit(IEngine *engine)
 {
     engine->threadAboutToStop().connect([](VirtualMachine *vm) {
         m_timeMap.erase(vm);
-
-        for (auto it = m_waitingBubbles.begin(); it != m_waitingBubbles.end();) {
-            if (it->second == vm)
-                m_waitingBubbles.erase(it);
-            else
-                it++;
-        }
+        erase_if(m_waitingBubbles, [vm](const std::pair<Target *, VirtualMachine *> &pair) { return pair.second == vm; });
     });
 }
 
diff --git a/src/blocks/soundblocks.cpp b/src/blocks/soundblocks.cpp
index 7106b6ca..fb999b87 100644
--- a/src/blocks/soundblocks.cpp
+++ b/src/blocks/soundblocks.cpp
@@ -10,6 +10,18 @@
 
 using namespace libscratchcpp;
 
+// TODO: Use C++20
+template<typename ContainerT, typename PredicateT>
+static void erase_if(ContainerT &items, const PredicateT &predicate)
+{
+    for (auto it = items.begin(); it != items.end();) {
+        if (predicate(*it))
+            it = items.erase(it);
+        else
+            ++it;
+    }
+}
+
 std::string SoundBlocks::name() const
 {
     return "Sound";
@@ -36,15 +48,7 @@ void SoundBlocks::registerBlocks(IEngine *engine)
 void SoundBlocks::onInit(IEngine *engine)
 {
     m_waitingSounds.clear();
-
-    engine->threadAboutToStop().connect([](VirtualMachine *vm) {
-        for (auto it = m_waitingSounds.begin(); it != m_waitingSounds.end();) {
-            if (it->second == vm)
-                m_waitingSounds.erase(it);
-            else
-                it++;
-        }
-    });
+    engine->threadAboutToStop().connect([](VirtualMachine *vm) { erase_if(m_waitingSounds, [vm](const std::pair<Sound *, VirtualMachine *> &pair) { return pair.second == vm; }); });
 }
 
 bool SoundBlocks::compilePlayCommon(Compiler *compiler, bool untilDone, bool *byIndex)

From 35ea917e3ac652c0e0335e292d1ab93b28ce250d Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Sun, 24 Mar 2024 19:28:43 +0100
Subject: [PATCH 54/77] README: Update roadmap

---
 README.md | 5 ++---
 1 file changed, 2 insertions(+), 3 deletions(-)

diff --git a/README.md b/README.md
index fe4b4a3d..78dc3387 100644
--- a/README.md
+++ b/README.md
@@ -139,10 +139,9 @@ libscratchcpp::Project p("https://scratch.mit.edu/projects/XXXXXX");
 - [x] API for monitors
 - [x] Project metadata
 - [x] Turbo mode
-- [ ] Multithreading (experimental)
 - [x] Custom FPS in the default event loop (`libscratchcpp::Engine::run()`)
-- [ ] Scratch 2.0 to 3.0 converter (help needed)
-- [ ] Scratch 1.4 and below to 3.0 converter (help needed)
+- [ ] Scratch 2.0 project loader (help needed)
+- [ ] Scratch 1.4 and below project loader (help needed)
 - [x] API for comments
 - [x] API for loading projects from URL
 

From 335990627320d432835fbf7711a0728c4d5a09da Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Mon, 25 Mar 2024 07:58:15 +0100
Subject: [PATCH 55/77] Move bounding rect methods to Target

---
 include/scratchcpp/istagehandler.h            |  9 ++++++++
 include/scratchcpp/sprite.h                   |  5 ++---
 include/scratchcpp/stage.h                    |  3 +++
 include/scratchcpp/target.h                   |  4 ++++
 src/scratch/sprite.cpp                        |  7 ++----
 src/scratch/stage.cpp                         | 18 +++++++++++++++
 src/scratch/target.cpp                        | 15 +++++++++++++
 test/mocks/stagehandlermock.h                 |  3 +++
 test/scratch_classes/stage_test.cpp           | 20 +++++++++++++++++
 test/scratch_classes/target_test.cpp          | 20 +++++++++++++++++
 test/target_interfaces/istagehandler_test.cpp | 22 +++++++++++++++++++
 11 files changed, 118 insertions(+), 8 deletions(-)

diff --git a/include/scratchcpp/istagehandler.h b/include/scratchcpp/istagehandler.h
index 354144d9..b0c4f799 100644
--- a/include/scratchcpp/istagehandler.h
+++ b/include/scratchcpp/istagehandler.h
@@ -43,6 +43,15 @@ class LIBSCRATCHCPP_EXPORT IStageHandler
 
         /*! Called when the bubble text changes. */
         virtual void onBubbleTextChanged(const std::string &text) = 0;
+
+        /*! Used to get the bounding rectangle of the stage. */
+        virtual Rect boundingRect() const = 0;
+
+        /*!
+         * Used to get a less accurate bounding rectangle of the stage
+         * which is calculated by transforming the costume rectangle.
+         */
+        virtual Rect fastBoundingRect() const = 0;
 };
 
 } // namespace libscratchcpp
diff --git a/include/scratchcpp/sprite.h b/include/scratchcpp/sprite.h
index 616c5636..ea467200 100644
--- a/include/scratchcpp/sprite.h
+++ b/include/scratchcpp/sprite.h
@@ -8,7 +8,6 @@ namespace libscratchcpp
 {
 
 class ISpriteHandler;
-class Rect;
 class IGraphicsEffect;
 class SpritePrivate;
 
@@ -69,8 +68,8 @@ class LIBSCRATCHCPP_EXPORT Sprite
 
         void setLayerOrder(int newLayerOrder) override;
 
-        Rect boundingRect() const;
-        Rect fastBoundingRect() const;
+        Rect boundingRect() const override;
+        Rect fastBoundingRect() const override;
         void keepInFence(double newX, double newY, double *fencedX, double *fencedY) const;
 
         void setGraphicsEffectValue(IGraphicsEffect *effect, double value) override;
diff --git a/include/scratchcpp/stage.h b/include/scratchcpp/stage.h
index 902f9b73..58f081d2 100644
--- a/include/scratchcpp/stage.h
+++ b/include/scratchcpp/stage.h
@@ -48,6 +48,9 @@ class LIBSCRATCHCPP_EXPORT Stage : public Target
         const std::string &textToSpeechLanguage() const;
         void setTextToSpeechLanguage(const std::string &newTextToSpeechLanguage);
 
+        Rect boundingRect() const override;
+        Rect fastBoundingRect() const override;
+
         void setGraphicsEffectValue(IGraphicsEffect *effect, double value) override;
 
         void clearGraphicsEffects() override;
diff --git a/include/scratchcpp/target.h b/include/scratchcpp/target.h
index 1b4a7874..fa489391 100644
--- a/include/scratchcpp/target.h
+++ b/include/scratchcpp/target.h
@@ -6,6 +6,7 @@
 
 #include "global.h"
 #include "spimpl.h"
+#include "rect.h"
 
 namespace libscratchcpp
 {
@@ -84,6 +85,9 @@ class LIBSCRATCHCPP_EXPORT Target
         double volume() const;
         void setVolume(double newVolume);
 
+        virtual Rect boundingRect() const;
+        virtual Rect fastBoundingRect() const;
+
         double graphicsEffectValue(IGraphicsEffect *effect) const;
         virtual void setGraphicsEffectValue(IGraphicsEffect *effect, double value);
 
diff --git a/src/scratch/sprite.cpp b/src/scratch/sprite.cpp
index 62043a95..4282f373 100644
--- a/src/scratch/sprite.cpp
+++ b/src/scratch/sprite.cpp
@@ -341,7 +341,7 @@ void Sprite::setLayerOrder(int newLayerOrder)
         impl->iface->onLayerOrderChanged(newLayerOrder);
 }
 
-/*! Returns the bounding rectangle of the sprite. */
+/*! Overrides Target#boundingRect(). */
 Rect Sprite::boundingRect() const
 {
     if (!impl->iface)
@@ -350,10 +350,7 @@ Rect Sprite::boundingRect() const
     return impl->iface->boundingRect();
 }
 
-/*!
- * Returns the less accurate bounding rectangle of the sprite
- * which is calculated by transforming the costume rectangle.
- */
+/*! Overrides Target#fastBoundingRect(). */
 Rect Sprite::fastBoundingRect() const
 {
     if (!impl->iface)
diff --git a/src/scratch/stage.cpp b/src/scratch/stage.cpp
index 7f55b70a..70c8d2cd 100644
--- a/src/scratch/stage.cpp
+++ b/src/scratch/stage.cpp
@@ -131,6 +131,24 @@ void Stage::setTextToSpeechLanguage(const std::string &newTextToSpeechLanguage)
     impl->textToSpeechLanguage = newTextToSpeechLanguage;
 }
 
+/*! Overrides Target#boundingRect(). */
+Rect Stage::boundingRect() const
+{
+    if (!impl->iface)
+        return Rect();
+
+    return impl->iface->boundingRect();
+}
+
+/*! Overrides Target#boundingRect(). */
+Rect Stage::fastBoundingRect() const
+{
+    if (!impl->iface)
+        return Rect();
+
+    return impl->iface->fastBoundingRect();
+}
+
 /*! Overrides Target#setGraphicsEffectValue(). */
 void Stage::setGraphicsEffectValue(IGraphicsEffect *effect, double value)
 {
diff --git a/src/scratch/target.cpp b/src/scratch/target.cpp
index 3392bef2..e93855f9 100644
--- a/src/scratch/target.cpp
+++ b/src/scratch/target.cpp
@@ -416,6 +416,21 @@ void Target::setVolume(double newVolume)
     }
 }
 
+/*! Returns the bounding rectangle of the sprite. */
+Rect Target::boundingRect() const
+{
+    return Rect();
+}
+
+/*!
+ * Returns the less accurate bounding rectangle of the sprite
+ * which is calculated by transforming the costume rectangle.
+ */
+Rect Target::fastBoundingRect() const
+{
+    return Rect();
+}
+
 /*! Returns the value of the given graphics effect. */
 double Target::graphicsEffectValue(IGraphicsEffect *effect) const
 {
diff --git a/test/mocks/stagehandlermock.h b/test/mocks/stagehandlermock.h
index d00c569c..55d993d4 100644
--- a/test/mocks/stagehandlermock.h
+++ b/test/mocks/stagehandlermock.h
@@ -19,4 +19,7 @@ class StageHandlerMock : public IStageHandler
         MOCK_METHOD(void, onGraphicsEffectsCleared, (), (override));
         MOCK_METHOD(void, onBubbleTypeChanged, (Target::BubbleType), (override));
         MOCK_METHOD(void, onBubbleTextChanged, (const std::string &), (override));
+
+        MOCK_METHOD(Rect, boundingRect, (), (const, override));
+        MOCK_METHOD(Rect, fastBoundingRect, (), (const, override));
 };
diff --git a/test/scratch_classes/stage_test.cpp b/test/scratch_classes/stage_test.cpp
index 2a0a6048..e716ea57 100644
--- a/test/scratch_classes/stage_test.cpp
+++ b/test/scratch_classes/stage_test.cpp
@@ -119,6 +119,26 @@ TEST(StageTest, TextToSpeechLanguage)
     ASSERT_EQ(stage.textToSpeechLanguage(), "English");
 }
 
+TEST(StageTest, DefaultBoundingRect)
+{
+    Stage stage;
+    Rect rect = stage.boundingRect();
+    ASSERT_EQ(rect.left(), 0);
+    ASSERT_EQ(rect.top(), 0);
+    ASSERT_EQ(rect.right(), 0);
+    ASSERT_EQ(rect.bottom(), 0);
+}
+
+TEST(StageTest, DefaultFastBoundingRect)
+{
+    Stage stage;
+    Rect rect = stage.fastBoundingRect();
+    ASSERT_EQ(rect.left(), 0);
+    ASSERT_EQ(rect.top(), 0);
+    ASSERT_EQ(rect.right(), 0);
+    ASSERT_EQ(rect.bottom(), 0);
+}
+
 TEST(StageTest, GraphicsEffects)
 {
     Stage stage;
diff --git a/test/scratch_classes/target_test.cpp b/test/scratch_classes/target_test.cpp
index 6924980d..a9d6ef55 100644
--- a/test/scratch_classes/target_test.cpp
+++ b/test/scratch_classes/target_test.cpp
@@ -560,6 +560,26 @@ TEST(TargetTest, Volume)
     SoundPrivate::audioOutput = nullptr;
 }
 
+TEST(TargetTest, BoundingRect)
+{
+    Target target;
+    Rect rect = target.boundingRect();
+    ASSERT_EQ(rect.left(), 0);
+    ASSERT_EQ(rect.top(), 0);
+    ASSERT_EQ(rect.right(), 0);
+    ASSERT_EQ(rect.bottom(), 0);
+}
+
+TEST(TargetTest, FastBoundingRect)
+{
+    Target target;
+    Rect rect = target.fastBoundingRect();
+    ASSERT_EQ(rect.left(), 0);
+    ASSERT_EQ(rect.top(), 0);
+    ASSERT_EQ(rect.right(), 0);
+    ASSERT_EQ(rect.bottom(), 0);
+}
+
 TEST(TargetTest, GraphicsEffects)
 {
     Target target;
diff --git a/test/target_interfaces/istagehandler_test.cpp b/test/target_interfaces/istagehandler_test.cpp
index a109624a..242211ec 100644
--- a/test/target_interfaces/istagehandler_test.cpp
+++ b/test/target_interfaces/istagehandler_test.cpp
@@ -8,6 +8,8 @@
 
 using namespace libscratchcpp;
 
+using ::testing::Return;
+
 class IStageHandlerTest : public testing::Test
 {
     public:
@@ -82,3 +84,23 @@ TEST_F(IStageHandlerTest, BubbleText)
     EXPECT_CALL(m_engine, requestRedraw());
     m_stage.setBubbleText("test");
 }
+
+TEST_F(IStageHandlerTest, BoundingRect)
+{
+    EXPECT_CALL(m_handler, boundingRect()).WillOnce(Return(Rect(-44.6, 89.1, 20.5, -0.48)));
+    Rect rect = m_stage.boundingRect();
+    ASSERT_EQ(rect.left(), -44.6);
+    ASSERT_EQ(rect.top(), 89.1);
+    ASSERT_EQ(rect.right(), 20.5);
+    ASSERT_EQ(rect.bottom(), -0.48);
+}
+
+TEST_F(IStageHandlerTest, FastBoundingRect)
+{
+    EXPECT_CALL(m_handler, fastBoundingRect()).WillOnce(Return(Rect(-44.6, 89.1, 20.5, -0.48)));
+    Rect rect = m_stage.fastBoundingRect();
+    ASSERT_EQ(rect.left(), -44.6);
+    ASSERT_EQ(rect.top(), 89.1);
+    ASSERT_EQ(rect.right(), 20.5);
+    ASSERT_EQ(rect.bottom(), -0.48);
+}

From c05edee954da5e7ae9070895ef4b4df57cad3761 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Mon, 25 Mar 2024 14:07:12 +0100
Subject: [PATCH 56/77] Add touchingEdge() method to Target

---
 include/scratchcpp/target.h          |  2 ++
 src/scratch/target.cpp               | 16 ++++++++++
 test/scratch_classes/target_test.cpp | 44 ++++++++++++++++++++++++++++
 3 files changed, 62 insertions(+)

diff --git a/include/scratchcpp/target.h b/include/scratchcpp/target.h
index fa489391..0b633ef6 100644
--- a/include/scratchcpp/target.h
+++ b/include/scratchcpp/target.h
@@ -88,6 +88,8 @@ class LIBSCRATCHCPP_EXPORT Target
         virtual Rect boundingRect() const;
         virtual Rect fastBoundingRect() const;
 
+        bool touchingEdge() const;
+
         double graphicsEffectValue(IGraphicsEffect *effect) const;
         virtual void setGraphicsEffectValue(IGraphicsEffect *effect, double value);
 
diff --git a/src/scratch/target.cpp b/src/scratch/target.cpp
index e93855f9..360f3cf9 100644
--- a/src/scratch/target.cpp
+++ b/src/scratch/target.cpp
@@ -431,6 +431,22 @@ Rect Target::fastBoundingRect() const
     return Rect();
 }
 
+/*! Returns true if the target is touching the edge. */
+bool Target::touchingEdge() const
+{
+    // https://github.com/scratchfoundation/scratch-vm/blob/8dbcc1fc8f8d8c4f1e40629fe8a388149d6dfd1c/src/sprites/rendered-target.js#L772-L785
+    if (impl->engine) {
+        const double stageWidth = impl->engine->stageWidth();
+        const double stageHeight = impl->engine->stageHeight();
+        Rect bounds = boundingRect();
+
+        if ((bounds.left() < -stageWidth / 2) || (bounds.right() > stageWidth / 2) || (bounds.top() > stageHeight / 2) || (bounds.bottom() < -stageHeight / 2))
+            return true;
+    }
+
+    return false;
+}
+
 /*! Returns the value of the given graphics effect. */
 double Target::graphicsEffectValue(IGraphicsEffect *effect) const
 {
diff --git a/test/scratch_classes/target_test.cpp b/test/scratch_classes/target_test.cpp
index a9d6ef55..eb28774c 100644
--- a/test/scratch_classes/target_test.cpp
+++ b/test/scratch_classes/target_test.cpp
@@ -1,4 +1,5 @@
 #include <scratchcpp/target.h>
+#include <scratchcpp/sprite.h>
 #include <scratchcpp/variable.h>
 #include <scratchcpp/list.h>
 #include <scratchcpp/block.h>
@@ -8,6 +9,7 @@
 #include <scratch/sound_p.h>
 #include <enginemock.h>
 #include <targetmock.h>
+#include <spritehandlermock.h>
 #include <graphicseffectmock.h>
 #include <audiooutputmock.h>
 #include <audioplayermock.h>
@@ -580,6 +582,48 @@ TEST(TargetTest, FastBoundingRect)
     ASSERT_EQ(rect.bottom(), 0);
 }
 
+TEST(TargetTest, TouchingEdge)
+{
+    Target target;
+    EngineMock engine;
+    EXPECT_CALL(engine, stageWidth()).WillRepeatedly(Return(480));
+    EXPECT_CALL(engine, stageHeight()).WillRepeatedly(Return(360));
+    ASSERT_FALSE(target.touchingEdge());
+    target.setEngine(&engine);
+    ASSERT_FALSE(target.touchingEdge());
+
+    Sprite sprite;
+    sprite.setEngine(&engine);
+    ASSERT_FALSE(sprite.touchingEdge());
+
+    SpriteHandlerMock iface;
+    EXPECT_CALL(iface, init);
+    sprite.setInterface(&iface);
+
+    EXPECT_CALL(iface, boundingRect).WillOnce(Return(Rect(-100, 100, 100, -100)));
+    ASSERT_FALSE(sprite.touchingEdge());
+    EXPECT_CALL(iface, boundingRect).WillOnce(Return(Rect(-240, 100, 100, -100)));
+    ASSERT_FALSE(sprite.touchingEdge());
+    EXPECT_CALL(iface, boundingRect).WillOnce(Return(Rect(-240.1, 100, 100, -100)));
+    ASSERT_TRUE(sprite.touchingEdge());
+    EXPECT_CALL(iface, boundingRect).WillOnce(Return(Rect(-240, 180, 100, -100)));
+    ASSERT_FALSE(sprite.touchingEdge());
+    EXPECT_CALL(iface, boundingRect).WillOnce(Return(Rect(-240, 180.1, 100, -100)));
+    ASSERT_TRUE(sprite.touchingEdge());
+    EXPECT_CALL(iface, boundingRect).WillOnce(Return(Rect(-240, 180, 240, -100)));
+    ASSERT_FALSE(sprite.touchingEdge());
+    EXPECT_CALL(iface, boundingRect).WillOnce(Return(Rect(-240, 180, 240.1, -100)));
+    ASSERT_TRUE(sprite.touchingEdge());
+    EXPECT_CALL(iface, boundingRect).WillOnce(Return(Rect(-240, 180, 240, -180)));
+    ASSERT_FALSE(sprite.touchingEdge());
+    EXPECT_CALL(iface, boundingRect).WillOnce(Return(Rect(-240, 180, 240, -180.1)));
+    ASSERT_TRUE(sprite.touchingEdge());
+    EXPECT_CALL(iface, boundingRect).WillOnce(Return(Rect(-242, 183, 240, -100)));
+    ASSERT_TRUE(sprite.touchingEdge());
+    EXPECT_CALL(iface, boundingRect).WillOnce(Return(Rect(-242, 183, 280, -690)));
+    ASSERT_TRUE(sprite.touchingEdge());
+}
+
 TEST(TargetTest, GraphicsEffects)
 {
     Target target;

From f49734e78c1661bb46ee7eb4b52b51a3284acb8e Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Mon, 25 Mar 2024 14:21:14 +0100
Subject: [PATCH 57/77] Add touchingPoint() method

---
 include/scratchcpp/ispritehandler.h  |  3 +++
 include/scratchcpp/istagehandler.h   |  3 +++
 include/scratchcpp/sprite.h          |  2 ++
 include/scratchcpp/stage.h           |  2 ++
 include/scratchcpp/target.h          |  1 +
 src/scratch/sprite.cpp               |  9 +++++++++
 src/scratch/stage.cpp                |  9 +++++++++
 src/scratch/target.cpp               |  6 ++++++
 test/mocks/spritehandlermock.h       |  1 +
 test/mocks/stagehandlermock.h        |  1 +
 test/scratch_classes/sprite_test.cpp | 16 ++++++++++++++++
 test/scratch_classes/stage_test.cpp  | 18 ++++++++++++++++++
 test/scratch_classes/target_test.cpp |  6 ++++++
 13 files changed, 77 insertions(+)

diff --git a/include/scratchcpp/ispritehandler.h b/include/scratchcpp/ispritehandler.h
index d4ec2fe3..5c56ba3f 100644
--- a/include/scratchcpp/ispritehandler.h
+++ b/include/scratchcpp/ispritehandler.h
@@ -77,6 +77,9 @@ class LIBSCRATCHCPP_EXPORT ISpriteHandler
          * \note The rectangle must be relative to the stage, so make sure to use the sprite's coordinates.
          */
         virtual Rect fastBoundingRect() const = 0;
+
+        /*! Used to check whether the sprite touches the given point (in Scratch coordinates). */
+        virtual bool touchingPoint(double x, double y) const = 0;
 };
 
 } // namespace libscratchcpp
diff --git a/include/scratchcpp/istagehandler.h b/include/scratchcpp/istagehandler.h
index b0c4f799..efbd1fde 100644
--- a/include/scratchcpp/istagehandler.h
+++ b/include/scratchcpp/istagehandler.h
@@ -52,6 +52,9 @@ class LIBSCRATCHCPP_EXPORT IStageHandler
          * which is calculated by transforming the costume rectangle.
          */
         virtual Rect fastBoundingRect() const = 0;
+
+        /*! Used to check whether the stage touches the given point (in Scratch coordinates). */
+        virtual bool touchingPoint(double x, double y) const = 0;
 };
 
 } // namespace libscratchcpp
diff --git a/include/scratchcpp/sprite.h b/include/scratchcpp/sprite.h
index ea467200..f42d32cc 100644
--- a/include/scratchcpp/sprite.h
+++ b/include/scratchcpp/sprite.h
@@ -72,6 +72,8 @@ class LIBSCRATCHCPP_EXPORT Sprite
         Rect fastBoundingRect() const override;
         void keepInFence(double newX, double newY, double *fencedX, double *fencedY) const;
 
+        bool touchingPoint(double x, double y) const override;
+
         void setGraphicsEffectValue(IGraphicsEffect *effect, double value) override;
 
         void clearGraphicsEffects() override;
diff --git a/include/scratchcpp/stage.h b/include/scratchcpp/stage.h
index 58f081d2..ebaa180a 100644
--- a/include/scratchcpp/stage.h
+++ b/include/scratchcpp/stage.h
@@ -51,6 +51,8 @@ class LIBSCRATCHCPP_EXPORT Stage : public Target
         Rect boundingRect() const override;
         Rect fastBoundingRect() const override;
 
+        bool touchingPoint(double x, double y) const override;
+
         void setGraphicsEffectValue(IGraphicsEffect *effect, double value) override;
 
         void clearGraphicsEffects() override;
diff --git a/include/scratchcpp/target.h b/include/scratchcpp/target.h
index 0b633ef6..5dd08a87 100644
--- a/include/scratchcpp/target.h
+++ b/include/scratchcpp/target.h
@@ -88,6 +88,7 @@ class LIBSCRATCHCPP_EXPORT Target
         virtual Rect boundingRect() const;
         virtual Rect fastBoundingRect() const;
 
+        virtual bool touchingPoint(double x, double y) const;
         bool touchingEdge() const;
 
         double graphicsEffectValue(IGraphicsEffect *effect) const;
diff --git a/src/scratch/sprite.cpp b/src/scratch/sprite.cpp
index 4282f373..b42ac010 100644
--- a/src/scratch/sprite.cpp
+++ b/src/scratch/sprite.cpp
@@ -406,6 +406,15 @@ void Sprite::keepInFence(double newX, double newY, double *fencedX, double *fenc
     *fencedY = newY + dy;
 }
 
+/*! Overrides Target#touchingPoint(). */
+bool Sprite::touchingPoint(double x, double y) const
+{
+    if (!impl->iface)
+        return false;
+
+    return impl->iface->touchingPoint(x, y);
+}
+
 /*! Overrides Target#setGraphicsEffectValue(). */
 void Sprite::setGraphicsEffectValue(IGraphicsEffect *effect, double value)
 {
diff --git a/src/scratch/stage.cpp b/src/scratch/stage.cpp
index 70c8d2cd..0f0d35c0 100644
--- a/src/scratch/stage.cpp
+++ b/src/scratch/stage.cpp
@@ -149,6 +149,15 @@ Rect Stage::fastBoundingRect() const
     return impl->iface->fastBoundingRect();
 }
 
+/*! Overrides Target#touchingPoint(). */
+bool Stage::touchingPoint(double x, double y) const
+{
+    if (!impl->iface)
+        return false;
+
+    return impl->iface->touchingPoint(x, y);
+}
+
 /*! Overrides Target#setGraphicsEffectValue(). */
 void Stage::setGraphicsEffectValue(IGraphicsEffect *effect, double value)
 {
diff --git a/src/scratch/target.cpp b/src/scratch/target.cpp
index 360f3cf9..ff4b7e49 100644
--- a/src/scratch/target.cpp
+++ b/src/scratch/target.cpp
@@ -431,6 +431,12 @@ Rect Target::fastBoundingRect() const
     return Rect();
 }
 
+/*! Returns true if the Target is touching the given point (in Scratch coordinates). */
+bool Target::touchingPoint(double x, double y) const
+{
+    return false;
+}
+
 /*! Returns true if the target is touching the edge. */
 bool Target::touchingEdge() const
 {
diff --git a/test/mocks/spritehandlermock.h b/test/mocks/spritehandlermock.h
index 93f212f6..888913e8 100644
--- a/test/mocks/spritehandlermock.h
+++ b/test/mocks/spritehandlermock.h
@@ -31,4 +31,5 @@ class SpriteHandlerMock : public ISpriteHandler
 
         MOCK_METHOD(Rect, boundingRect, (), (const, override));
         MOCK_METHOD(Rect, fastBoundingRect, (), (const, override));
+        MOCK_METHOD(bool, touchingPoint, (double, double), (const, override));
 };
diff --git a/test/mocks/stagehandlermock.h b/test/mocks/stagehandlermock.h
index 55d993d4..8c29dfa7 100644
--- a/test/mocks/stagehandlermock.h
+++ b/test/mocks/stagehandlermock.h
@@ -22,4 +22,5 @@ class StageHandlerMock : public IStageHandler
 
         MOCK_METHOD(Rect, boundingRect, (), (const, override));
         MOCK_METHOD(Rect, fastBoundingRect, (), (const, override));
+        MOCK_METHOD(bool, touchingPoint, (double, double), (const, override));
 };
diff --git a/test/scratch_classes/sprite_test.cpp b/test/scratch_classes/sprite_test.cpp
index 04c15da1..68890a04 100644
--- a/test/scratch_classes/sprite_test.cpp
+++ b/test/scratch_classes/sprite_test.cpp
@@ -614,6 +614,22 @@ TEST(SpriteTest, DefaultFastBoundingRect)
     ASSERT_EQ(rect.bottom(), -45.01);
 }
 
+TEST(SpriteTest, TouchingPoint)
+{
+    Sprite sprite;
+    ASSERT_FALSE(sprite.touchingPoint(0, 0));
+
+    SpriteHandlerMock iface;
+    EXPECT_CALL(iface, init);
+    sprite.setInterface(&iface);
+
+    EXPECT_CALL(iface, touchingPoint(51.4, -74.05)).WillOnce(Return(false));
+    ASSERT_FALSE(sprite.touchingPoint(51.4, -74.05));
+
+    EXPECT_CALL(iface, touchingPoint(-12.46, 120.72)).WillOnce(Return(true));
+    ASSERT_TRUE(sprite.touchingPoint(-12.46, 120.72));
+}
+
 TEST(SpriteTest, GraphicsEffects)
 {
     Sprite sprite;
diff --git a/test/scratch_classes/stage_test.cpp b/test/scratch_classes/stage_test.cpp
index e716ea57..c04c0c74 100644
--- a/test/scratch_classes/stage_test.cpp
+++ b/test/scratch_classes/stage_test.cpp
@@ -8,6 +8,8 @@
 
 using namespace libscratchcpp;
 
+using ::testing::Return;
+
 TEST(StageTest, IsStage)
 {
     Stage stage;
@@ -139,6 +141,22 @@ TEST(StageTest, DefaultFastBoundingRect)
     ASSERT_EQ(rect.bottom(), 0);
 }
 
+TEST(StageTest, TouchingPoint)
+{
+    Stage stage;
+    ASSERT_FALSE(stage.touchingPoint(0, 0));
+
+    StageHandlerMock iface;
+    EXPECT_CALL(iface, init);
+    stage.setInterface(&iface);
+
+    EXPECT_CALL(iface, touchingPoint(51.4, -74.05)).WillOnce(Return(false));
+    ASSERT_FALSE(stage.touchingPoint(51.4, -74.05));
+
+    EXPECT_CALL(iface, touchingPoint(-12.46, 120.72)).WillOnce(Return(true));
+    ASSERT_TRUE(stage.touchingPoint(-12.46, 120.72));
+}
+
 TEST(StageTest, GraphicsEffects)
 {
     Stage stage;
diff --git a/test/scratch_classes/target_test.cpp b/test/scratch_classes/target_test.cpp
index eb28774c..df2e2a69 100644
--- a/test/scratch_classes/target_test.cpp
+++ b/test/scratch_classes/target_test.cpp
@@ -582,6 +582,12 @@ TEST(TargetTest, FastBoundingRect)
     ASSERT_EQ(rect.bottom(), 0);
 }
 
+TEST(TargetTest, TouchingPoint)
+{
+    Target target;
+    ASSERT_FALSE(target.touchingPoint(0, 0));
+}
+
 TEST(TargetTest, TouchingEdge)
 {
     Target target;

From b0609ad87df2ecd4dff86ba317edaae895fff070 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Mon, 25 Mar 2024 17:03:44 +0100
Subject: [PATCH 58/77] Add more methods to TargetMock

---
 test/mocks/targetmock.h | 18 +++++++++++++++++-
 1 file changed, 17 insertions(+), 1 deletion(-)

diff --git a/test/mocks/targetmock.h b/test/mocks/targetmock.h
index d1ee5459..ad774600 100644
--- a/test/mocks/targetmock.h
+++ b/test/mocks/targetmock.h
@@ -8,7 +8,23 @@ using namespace libscratchcpp;
 class TargetMock : public Target
 {
     public:
-        MOCK_METHOD(Target *, dataSource, (), (const override));
+        MOCK_METHOD(bool, isStage, (), (const, override));
+
+        MOCK_METHOD(void, setCostumeIndex, (int), (override));
+        MOCK_METHOD(void, setLayerOrder, (int), (override));
+
+        MOCK_METHOD(Rect, boundingRect, (), (const, override));
+        MOCK_METHOD(Rect, fastBoundingRect, (), (const, override));
+
+        MOCK_METHOD(bool, touchingPoint, (double, double), (const, override));
+
+        MOCK_METHOD(void, setGraphicsEffectValue, (IGraphicsEffect *, double), (override));
+        MOCK_METHOD(void, clearGraphicsEffects, (), (override));
+
+        MOCK_METHOD(void, setBubbleType, (BubbleType), (override));
+        MOCK_METHOD(void, setBubbleText, (const std::string &), (override));
+
+        MOCK_METHOD(Target *, dataSource, (), (const, override));
 
         Target *dataSourceBase() const { return Target::dataSource(); };
 };

From d669ce5d1a5618f0f77dc6b1a75bf2053735e0b0 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Mon, 25 Mar 2024 17:09:53 +0100
Subject: [PATCH 59/77] Add touchingSprite() method

---
 include/scratchcpp/ispritehandler.h  |  3 +++
 include/scratchcpp/istagehandler.h   |  5 ++++
 include/scratchcpp/sprite.h          |  1 +
 include/scratchcpp/stage.h           |  2 ++
 include/scratchcpp/target.h          |  5 ++++
 src/scratch/sprite.cpp               |  8 ++++++
 src/scratch/stage.cpp                |  8 ++++++
 src/scratch/target.cpp               | 23 ++++++++++++++++
 test/mocks/spritehandlermock.h       |  2 ++
 test/mocks/stagehandlermock.h        |  2 ++
 test/mocks/targetmock.h              |  2 ++
 test/scratch_classes/sprite_test.cpp | 37 ++++++++++++++++++++++++++
 test/scratch_classes/stage_test.cpp  | 38 +++++++++++++++++++++++++++
 test/scratch_classes/target_test.cpp | 39 ++++++++++++++++++++++++++++
 14 files changed, 175 insertions(+)

diff --git a/include/scratchcpp/ispritehandler.h b/include/scratchcpp/ispritehandler.h
index 5c56ba3f..3f8fe20b 100644
--- a/include/scratchcpp/ispritehandler.h
+++ b/include/scratchcpp/ispritehandler.h
@@ -78,6 +78,9 @@ class LIBSCRATCHCPP_EXPORT ISpriteHandler
          */
         virtual Rect fastBoundingRect() const = 0;
 
+        /*! Used to check whether the sprite touches any of the given sprite clones. */
+        virtual bool touchingClones(const std::vector<Sprite *> &clones) const = 0;
+
         /*! Used to check whether the sprite touches the given point (in Scratch coordinates). */
         virtual bool touchingPoint(double x, double y) const = 0;
 };
diff --git a/include/scratchcpp/istagehandler.h b/include/scratchcpp/istagehandler.h
index efbd1fde..aaf146f6 100644
--- a/include/scratchcpp/istagehandler.h
+++ b/include/scratchcpp/istagehandler.h
@@ -8,6 +8,8 @@
 namespace libscratchcpp
 {
 
+class Sprite;
+
 /*! \brief The IStageHandler class provides a stage interface for Scratch project players. */
 class LIBSCRATCHCPP_EXPORT IStageHandler
 {
@@ -53,6 +55,9 @@ class LIBSCRATCHCPP_EXPORT IStageHandler
          */
         virtual Rect fastBoundingRect() const = 0;
 
+        /*! Used to check whether the stage touches any of the given sprite clones. */
+        virtual bool touchingClones(const std::vector<Sprite *> &clones) const = 0;
+
         /*! Used to check whether the stage touches the given point (in Scratch coordinates). */
         virtual bool touchingPoint(double x, double y) const = 0;
 };
diff --git a/include/scratchcpp/sprite.h b/include/scratchcpp/sprite.h
index f42d32cc..6328c9fe 100644
--- a/include/scratchcpp/sprite.h
+++ b/include/scratchcpp/sprite.h
@@ -83,6 +83,7 @@ class LIBSCRATCHCPP_EXPORT Sprite
 
     private:
         Target *dataSource() const override;
+        bool touchingClones(const std::vector<Sprite *> &clones) const override;
         void setXY(double x, double y);
 
         spimpl::unique_impl_ptr<SpritePrivate> impl;
diff --git a/include/scratchcpp/stage.h b/include/scratchcpp/stage.h
index ebaa180a..cae0d396 100644
--- a/include/scratchcpp/stage.h
+++ b/include/scratchcpp/stage.h
@@ -61,6 +61,8 @@ class LIBSCRATCHCPP_EXPORT Stage : public Target
         virtual void setBubbleText(const std::string &text) override;
 
     private:
+        bool touchingClones(const std::vector<Sprite *> &clones) const override;
+
         spimpl::unique_impl_ptr<StagePrivate> impl;
 };
 
diff --git a/include/scratchcpp/target.h b/include/scratchcpp/target.h
index 5dd08a87..f3e65c9c 100644
--- a/include/scratchcpp/target.h
+++ b/include/scratchcpp/target.h
@@ -18,6 +18,7 @@ class Block;
 class Comment;
 class Costume;
 class Sound;
+class Sprite;
 class IGraphicsEffect;
 class TargetPrivate;
 
@@ -88,6 +89,7 @@ class LIBSCRATCHCPP_EXPORT Target
         virtual Rect boundingRect() const;
         virtual Rect fastBoundingRect() const;
 
+        bool touchingSprite(Sprite *sprite) const;
         virtual bool touchingPoint(double x, double y) const;
         bool touchingEdge() const;
 
@@ -109,6 +111,9 @@ class LIBSCRATCHCPP_EXPORT Target
         /*! Override this method to set a custom data source for blocks, assets, comments, etc. */
         virtual Target *dataSource() const { return nullptr; }
 
+        /*! Override this method to check whether the target touches the given sprite clones. */
+        virtual bool touchingClones(const std::vector<Sprite *> &clones) const { return false; }
+
     private:
         spimpl::unique_impl_ptr<TargetPrivate> impl;
 };
diff --git a/src/scratch/sprite.cpp b/src/scratch/sprite.cpp
index b42ac010..ce76a99f 100644
--- a/src/scratch/sprite.cpp
+++ b/src/scratch/sprite.cpp
@@ -477,6 +477,14 @@ Target *Sprite::dataSource() const
     return impl->cloneSprite;
 }
 
+bool Sprite::touchingClones(const std::vector<Sprite *> &clones) const
+{
+    if (!impl->iface)
+        return false;
+
+    return impl->iface->touchingClones(clones);
+}
+
 void Sprite::setXY(double x, double y)
 {
     IEngine *eng = engine();
diff --git a/src/scratch/stage.cpp b/src/scratch/stage.cpp
index 0f0d35c0..d927964a 100644
--- a/src/scratch/stage.cpp
+++ b/src/scratch/stage.cpp
@@ -208,3 +208,11 @@ void Stage::setBubbleText(const std::string &text)
     if (impl->iface)
         impl->iface->onBubbleTextChanged(text);
 }
+
+bool Stage::touchingClones(const std::vector<Sprite *> &clones) const
+{
+    if (!impl->iface)
+        return false;
+
+    return impl->iface->touchingClones(clones);
+}
diff --git a/src/scratch/target.cpp b/src/scratch/target.cpp
index ff4b7e49..8bfbd2c7 100644
--- a/src/scratch/target.cpp
+++ b/src/scratch/target.cpp
@@ -1,6 +1,7 @@
 // SPDX-License-Identifier: Apache-2.0
 
 #include <scratchcpp/target.h>
+#include <scratchcpp/sprite.h>
 #include <scratchcpp/variable.h>
 #include <scratchcpp/list.h>
 #include <scratchcpp/block.h>
@@ -431,6 +432,28 @@ Rect Target::fastBoundingRect() const
     return Rect();
 }
 
+/*! Returns true if the Target is touching the given Sprite (or its clones). */
+bool Target::touchingSprite(Sprite *sprite) const
+{
+    // https://github.com/scratchfoundation/scratch-vm/blob/8dbcc1fc8f8d8c4f1e40629fe8a388149d6dfd1c/src/sprites/rendered-target.js#L792-L805
+    if (!sprite)
+        return false;
+
+    Sprite *firstClone = sprite->isClone() ? sprite->cloneSprite() : sprite;
+    assert(firstClone);
+    std::vector<Sprite *> clones;
+
+    if (true) // TODO: Filter clones that are being dragged, including firstClone
+        clones.push_back(firstClone);
+
+    for (auto clone : firstClone->clones()) {
+        if (true) // TODO: Filter clones that are being dragged
+            clones.push_back(clone.get());
+    }
+
+    return touchingClones(clones);
+}
+
 /*! Returns true if the Target is touching the given point (in Scratch coordinates). */
 bool Target::touchingPoint(double x, double y) const
 {
diff --git a/test/mocks/spritehandlermock.h b/test/mocks/spritehandlermock.h
index 888913e8..bf01381a 100644
--- a/test/mocks/spritehandlermock.h
+++ b/test/mocks/spritehandlermock.h
@@ -31,5 +31,7 @@ class SpriteHandlerMock : public ISpriteHandler
 
         MOCK_METHOD(Rect, boundingRect, (), (const, override));
         MOCK_METHOD(Rect, fastBoundingRect, (), (const, override));
+
+        MOCK_METHOD(bool, touchingClones, (const std::vector<Sprite *> &), (const, override));
         MOCK_METHOD(bool, touchingPoint, (double, double), (const, override));
 };
diff --git a/test/mocks/stagehandlermock.h b/test/mocks/stagehandlermock.h
index 8c29dfa7..0f170c61 100644
--- a/test/mocks/stagehandlermock.h
+++ b/test/mocks/stagehandlermock.h
@@ -22,5 +22,7 @@ class StageHandlerMock : public IStageHandler
 
         MOCK_METHOD(Rect, boundingRect, (), (const, override));
         MOCK_METHOD(Rect, fastBoundingRect, (), (const, override));
+
+        MOCK_METHOD(bool, touchingClones, (const std::vector<Sprite *> &), (const, override));
         MOCK_METHOD(bool, touchingPoint, (double, double), (const, override));
 };
diff --git a/test/mocks/targetmock.h b/test/mocks/targetmock.h
index ad774600..bcf1cef7 100644
--- a/test/mocks/targetmock.h
+++ b/test/mocks/targetmock.h
@@ -27,4 +27,6 @@ class TargetMock : public Target
         MOCK_METHOD(Target *, dataSource, (), (const, override));
 
         Target *dataSourceBase() const { return Target::dataSource(); };
+
+        MOCK_METHOD(bool, touchingClones, (const std::vector<Sprite *> &), (const, override));
 };
diff --git a/test/scratch_classes/sprite_test.cpp b/test/scratch_classes/sprite_test.cpp
index 68890a04..61b79be9 100644
--- a/test/scratch_classes/sprite_test.cpp
+++ b/test/scratch_classes/sprite_test.cpp
@@ -8,6 +8,7 @@
 #include <enginemock.h>
 #include <graphicseffectmock.h>
 #include <spritehandlermock.h>
+#include <targetmock.h>
 
 #include "../common.h"
 
@@ -16,6 +17,9 @@ using namespace libscratchcpp;
 using ::testing::_;
 using ::testing::SaveArg;
 using ::testing::Return;
+using ::testing::WithArgs;
+using ::testing::Invoke;
+using ::testing::_;
 
 TEST(SpriteTest, IsStage)
 {
@@ -614,6 +618,39 @@ TEST(SpriteTest, DefaultFastBoundingRect)
     ASSERT_EQ(rect.bottom(), -45.01);
 }
 
+TEST(SpriteTest, TouchingSprite)
+{
+    Sprite sprite;
+    Sprite another;
+    EngineMock engine;
+    another.setEngine(&engine);
+    ASSERT_FALSE(sprite.touchingSprite(nullptr));
+
+    SpriteHandlerMock iface;
+    EXPECT_CALL(iface, init);
+    sprite.setInterface(&iface);
+
+    EXPECT_CALL(engine, cloneLimit()).WillRepeatedly(Return(-1));
+    EXPECT_CALL(engine, initClone).Times(3);
+    EXPECT_CALL(engine, requestRedraw).Times(3);
+    EXPECT_CALL(engine, moveSpriteBehindOther).Times(3);
+    auto clone1 = another.clone();
+    auto clone2 = another.clone();
+    auto clone3 = another.clone();
+    std::vector<Sprite *> clones = { &another, clone1.get(), clone2.get(), clone3.get() };
+    std::vector<Sprite *> actualClones;
+
+    EXPECT_CALL(iface, touchingClones(_)).WillOnce(WithArgs<0>(Invoke([&clones, &actualClones](const std::vector<Sprite *> &candidates) {
+        actualClones = candidates;
+        return false;
+    })));
+    ASSERT_FALSE(sprite.touchingSprite(&another));
+    ASSERT_EQ(clones, actualClones);
+
+    EXPECT_CALL(iface, touchingClones).WillOnce(Return(true));
+    ASSERT_TRUE(sprite.touchingSprite(&another));
+}
+
 TEST(SpriteTest, TouchingPoint)
 {
     Sprite sprite;
diff --git a/test/scratch_classes/stage_test.cpp b/test/scratch_classes/stage_test.cpp
index c04c0c74..2cfe63ca 100644
--- a/test/scratch_classes/stage_test.cpp
+++ b/test/scratch_classes/stage_test.cpp
@@ -1,14 +1,19 @@
 #include <scratchcpp/stage.h>
+#include <scratchcpp/sprite.h>
 #include <scratchcpp/costume.h>
 #include <enginemock.h>
 #include <graphicseffectmock.h>
 #include <stagehandlermock.h>
+#include <targetmock.h>
 
 #include "../common.h"
 
 using namespace libscratchcpp;
 
 using ::testing::Return;
+using ::testing::WithArgs;
+using ::testing::Invoke;
+using ::testing::_;
 
 TEST(StageTest, IsStage)
 {
@@ -141,6 +146,39 @@ TEST(StageTest, DefaultFastBoundingRect)
     ASSERT_EQ(rect.bottom(), 0);
 }
 
+TEST(StageTest, TouchingSprite)
+{
+    Stage stage;
+    Sprite another;
+    EngineMock engine;
+    another.setEngine(&engine);
+    ASSERT_FALSE(stage.touchingSprite(nullptr));
+
+    StageHandlerMock iface;
+    EXPECT_CALL(iface, init);
+    stage.setInterface(&iface);
+
+    EXPECT_CALL(engine, cloneLimit()).WillRepeatedly(Return(-1));
+    EXPECT_CALL(engine, initClone).Times(3);
+    EXPECT_CALL(engine, requestRedraw).Times(3);
+    EXPECT_CALL(engine, moveSpriteBehindOther).Times(3);
+    auto clone1 = another.clone();
+    auto clone2 = another.clone();
+    auto clone3 = another.clone();
+    std::vector<Sprite *> clones = { &another, clone1.get(), clone2.get(), clone3.get() };
+    std::vector<Sprite *> actualClones;
+
+    EXPECT_CALL(iface, touchingClones(_)).WillOnce(WithArgs<0>(Invoke([&clones, &actualClones](const std::vector<Sprite *> &candidates) {
+        actualClones = candidates;
+        return false;
+    })));
+    ASSERT_FALSE(stage.touchingSprite(&another));
+    ASSERT_EQ(clones, actualClones);
+
+    EXPECT_CALL(iface, touchingClones).WillOnce(Return(true));
+    ASSERT_TRUE(stage.touchingSprite(&another));
+}
+
 TEST(StageTest, TouchingPoint)
 {
     Stage stage;
diff --git a/test/scratch_classes/target_test.cpp b/test/scratch_classes/target_test.cpp
index df2e2a69..9957cf65 100644
--- a/test/scratch_classes/target_test.cpp
+++ b/test/scratch_classes/target_test.cpp
@@ -19,6 +19,9 @@
 using namespace libscratchcpp;
 
 using ::testing::Return;
+using ::testing::WithArgs;
+using ::testing::Invoke;
+using ::testing::_;
 
 TEST(TargetTest, IsStage)
 {
@@ -582,6 +585,42 @@ TEST(TargetTest, FastBoundingRect)
     ASSERT_EQ(rect.bottom(), 0);
 }
 
+TEST(TargetTest, TouchingSprite)
+{
+    TargetMock target;
+    Sprite sprite;
+    EngineMock engine;
+    sprite.setEngine(&engine);
+    ASSERT_FALSE(target.touchingSprite(nullptr));
+
+    EXPECT_CALL(engine, cloneLimit()).WillRepeatedly(Return(-1));
+    EXPECT_CALL(engine, initClone).Times(3);
+    EXPECT_CALL(engine, requestRedraw).Times(3);
+    EXPECT_CALL(engine, moveSpriteBehindOther).Times(3);
+    auto clone1 = sprite.clone();
+    auto clone2 = sprite.clone();
+    auto clone3 = sprite.clone();
+    std::vector<Sprite *> clones = { &sprite, clone1.get(), clone2.get(), clone3.get() };
+    std::vector<Sprite *> actualClones;
+
+    EXPECT_CALL(target, touchingClones(_)).WillOnce(WithArgs<0>(Invoke([&clones, &actualClones](const std::vector<Sprite *> &candidates) {
+        actualClones = candidates;
+        return false;
+    })));
+    ASSERT_FALSE(target.touchingSprite(&sprite));
+    ASSERT_EQ(clones, actualClones);
+
+    EXPECT_CALL(target, touchingClones).WillOnce(Return(true));
+    ASSERT_TRUE(target.touchingSprite(&sprite));
+
+    EXPECT_CALL(target, touchingClones(_)).WillOnce(WithArgs<0>(Invoke([&clones, &actualClones](const std::vector<Sprite *> &candidates) {
+        actualClones = candidates;
+        return true;
+    })));
+    ASSERT_TRUE(target.touchingSprite(clone2.get()));
+    ASSERT_EQ(clones, actualClones);
+}
+
 TEST(TargetTest, TouchingPoint)
 {
     Target target;

From 90311da2809b222b22ea3695ae0d099de034b81d Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Mon, 25 Mar 2024 18:20:44 +0100
Subject: [PATCH 60/77] Fix test names in stage test

---
 test/scratch_classes/stage_test.cpp | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/test/scratch_classes/stage_test.cpp b/test/scratch_classes/stage_test.cpp
index 2cfe63ca..747395af 100644
--- a/test/scratch_classes/stage_test.cpp
+++ b/test/scratch_classes/stage_test.cpp
@@ -21,7 +21,7 @@ TEST(StageTest, IsStage)
     ASSERT_TRUE(stage.isStage());
 }
 
-TEST(SpriteTest, Interface)
+TEST(StageTest, Interface)
 {
     Stage stage;
     ASSERT_EQ(stage.getInterface(), nullptr);
@@ -32,7 +32,7 @@ TEST(SpriteTest, Interface)
     ASSERT_EQ(stage.getInterface(), &handler);
 }
 
-TEST(SpriteTest, CostumeIndex)
+TEST(StageTest, CostumeIndex)
 {
     Stage stage;
     EngineMock engine;

From 5f3553ddd5ffe325988c3f3c232075111c8de932 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Tue, 26 Mar 2024 19:40:42 +0100
Subject: [PATCH 61/77] Implement sensing_touchingobject block

---
 src/blocks/sensingblocks.cpp        |  68 ++++++++++
 src/blocks/sensingblocks.h          |   9 ++
 test/blocks/sensing_blocks_test.cpp | 189 ++++++++++++++++++++++++++++
 3 files changed, 266 insertions(+)

diff --git a/src/blocks/sensingblocks.cpp b/src/blocks/sensingblocks.cpp
index 7b91c8b0..e052064d 100644
--- a/src/blocks/sensingblocks.cpp
+++ b/src/blocks/sensingblocks.cpp
@@ -30,6 +30,7 @@ std::string SensingBlocks::name() const
 void SensingBlocks::registerBlocks(IEngine *engine)
 {
     // Blocks
+    engine->addCompileFunction(this, "sensing_touchingobject", &compileTouchingObject);
     engine->addCompileFunction(this, "sensing_distanceto", &compileDistanceTo);
     engine->addCompileFunction(this, "sensing_askandwait", &compileAskAndWait);
     engine->addCompileFunction(this, "sensing_answer", &compileAnswer);
@@ -56,6 +57,7 @@ void SensingBlocks::registerBlocks(IEngine *engine)
     engine->addMonitorNameFunction(this, "sensing_dayssince2000", &daysSince2000MonitorName);
 
     // Inputs
+    engine->addInput(this, "TOUCHINGOBJECTMENU", TOUCHINGOBJECTMENU);
     engine->addInput(this, "DISTANCETOMENU", DISTANCETOMENU);
     engine->addInput(this, "QUESTION", QUESTION);
     engine->addInput(this, "KEY_OPTION", KEY_OPTION);
@@ -91,6 +93,29 @@ void SensingBlocks::registerBlocks(IEngine *engine)
     engine->questionAnswered().connect(&onAnswer);
 }
 
+void SensingBlocks::compileTouchingObject(Compiler *compiler)
+{
+    Input *input = compiler->input(TOUCHINGOBJECTMENU);
+
+    if (input->type() != Input::Type::ObscuredShadow) {
+        assert(input->pointsToDropdownMenu());
+        std::string value = input->selectedMenuItem();
+
+        if (value == "_mouse_")
+            compiler->addFunctionCall(&touchingMousePointer);
+        else if (value == "_edge_")
+            compiler->addFunctionCall(&touchingEdge);
+        else {
+            int index = compiler->engine()->findTarget(value);
+            compiler->addConstValue(index);
+            compiler->addFunctionCall(&touchingObjectByIndex);
+        }
+    } else {
+        compiler->addInput(input);
+        compiler->addFunctionCall(&touchingObject);
+    }
+}
+
 void SensingBlocks::compileDistanceTo(Compiler *compiler)
 {
     Input *input = compiler->input(DISTANCETOMENU);
@@ -435,6 +460,41 @@ const std::string &SensingBlocks::daysSince2000MonitorName(Block *block)
     return name;
 }
 
+unsigned int SensingBlocks::touchingObject(VirtualMachine *vm)
+{
+    std::string value = vm->getInput(0, 1)->toString();
+
+    if (value == "_mouse_")
+        vm->replaceReturnValue(vm->target()->touchingPoint(vm->engine()->mouseX(), vm->engine()->mouseY()), 1);
+    else if (value == "_edge_")
+        vm->replaceReturnValue(vm->target()->touchingEdge(), 1);
+    else {
+        Target *target = vm->engine()->targetAt(vm->engine()->findTarget(value));
+        vm->replaceReturnValue(touchingObjectCommon(vm->target(), target), 1);
+    }
+
+    return 0;
+}
+
+unsigned int SensingBlocks::touchingObjectByIndex(VirtualMachine *vm)
+{
+    Target *target = vm->engine()->targetAt(vm->getInput(0, 1)->toInt());
+    vm->replaceReturnValue(touchingObjectCommon(vm->target(), target), 1);
+    return 0;
+}
+
+unsigned int SensingBlocks::touchingMousePointer(VirtualMachine *vm)
+{
+    vm->addReturnValue(vm->target()->touchingPoint(vm->engine()->mouseX(), vm->engine()->mouseY()));
+    return 0;
+}
+
+unsigned int SensingBlocks::touchingEdge(VirtualMachine *vm)
+{
+    vm->addReturnValue(vm->target()->touchingEdge());
+    return 0;
+}
+
 unsigned int SensingBlocks::keyPressed(VirtualMachine *vm)
 {
     vm->replaceReturnValue(vm->engine()->keyPressed(vm->getInput(0, 1)->toString()), 1);
@@ -921,6 +981,14 @@ unsigned int SensingBlocks::daysSince2000(VirtualMachine *vm)
     return 0;
 }
 
+bool SensingBlocks::touchingObjectCommon(Target *source, Target *target)
+{
+    if (source && target && !target->isStage())
+        return source->touchingSprite(static_cast<Sprite *>(target));
+
+    return false;
+}
+
 void SensingBlocks::enqueueAsk(const std::string &question, VirtualMachine *vm)
 {
     // https://github.com/scratchfoundation/scratch-vm/blob/6055823f203a696165084b873e661713806583ec/src/blocks/scratch3_sensing.js#L117-L119
diff --git a/src/blocks/sensingblocks.h b/src/blocks/sensingblocks.h
index c0cba564..f4590607 100644
--- a/src/blocks/sensingblocks.h
+++ b/src/blocks/sensingblocks.h
@@ -20,6 +20,7 @@ class SensingBlocks : public IBlockSection
     public:
         enum Inputs
         {
+            TOUCHINGOBJECTMENU,
             DISTANCETOMENU,
             QUESTION,
             KEY_OPTION,
@@ -59,6 +60,7 @@ class SensingBlocks : public IBlockSection
 
         void registerBlocks(IEngine *engine) override;
 
+        static void compileTouchingObject(Compiler *compiler);
         static void compileDistanceTo(Compiler *compiler);
         static void compileAskAndWait(Compiler *compiler);
         static void compileAnswer(Compiler *compiler);
@@ -83,6 +85,11 @@ class SensingBlocks : public IBlockSection
         static const std::string &currentMonitorName(Block *block);
         static const std::string &daysSince2000MonitorName(Block *block);
 
+        static unsigned int touchingObject(VirtualMachine *vm);
+        static unsigned int touchingObjectByIndex(VirtualMachine *vm);
+        static unsigned int touchingMousePointer(VirtualMachine *vm);
+        static unsigned int touchingEdge(VirtualMachine *vm);
+
         static unsigned int keyPressed(VirtualMachine *vm);
         static unsigned int mouseDown(VirtualMachine *vm);
         static unsigned int mouseX(VirtualMachine *vm);
@@ -154,6 +161,8 @@ class SensingBlocks : public IBlockSection
                 bool wasStage = false;
         };
 
+        static bool touchingObjectCommon(Target *source, Target *target);
+
         static void enqueueAsk(const std::string &question, VirtualMachine *vm);
         static void askNextQuestion();
 
diff --git a/test/blocks/sensing_blocks_test.cpp b/test/blocks/sensing_blocks_test.cpp
index ad13634f..8857c759 100644
--- a/test/blocks/sensing_blocks_test.cpp
+++ b/test/blocks/sensing_blocks_test.cpp
@@ -11,6 +11,7 @@
 #include <clockmock.h>
 #include <audioinputmock.h>
 #include <audioloudnessmock.h>
+#include <targetmock.h>
 
 #include "../common.h"
 #include "blocks/sensingblocks.h"
@@ -115,6 +116,7 @@ TEST_F(SensingBlocksTest, CategoryVisible)
 TEST_F(SensingBlocksTest, RegisterBlocks)
 {
     // Blocks
+    EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "sensing_touchingobject", &SensingBlocks::compileTouchingObject));
     EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "sensing_distanceto", &SensingBlocks::compileDistanceTo));
     EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "sensing_askandwait", &SensingBlocks::compileAskAndWait));
     EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "sensing_answer", &SensingBlocks::compileAnswer));
@@ -141,6 +143,7 @@ TEST_F(SensingBlocksTest, RegisterBlocks)
     EXPECT_CALL(m_engineMock, addMonitorNameFunction(m_section.get(), "sensing_dayssince2000", &SensingBlocks::daysSince2000MonitorName));
 
     // Inputs
+    EXPECT_CALL(m_engineMock, addInput(m_section.get(), "TOUCHINGOBJECTMENU", SensingBlocks::TOUCHINGOBJECTMENU));
     EXPECT_CALL(m_engineMock, addInput(m_section.get(), "DISTANCETOMENU", SensingBlocks::DISTANCETOMENU));
     EXPECT_CALL(m_engineMock, addInput(m_section.get(), "QUESTION", SensingBlocks::QUESTION));
     EXPECT_CALL(m_engineMock, addInput(m_section.get(), "KEY_OPTION", SensingBlocks::KEY_OPTION));
@@ -182,6 +185,192 @@ TEST_F(SensingBlocksTest, RegisterBlocks)
     ASSERT_EQ(questionAnswered.disconnect(&SensingBlocks::onAnswer), 1);
 }
 
+TEST_F(SensingBlocksTest, TouchingObject)
+{
+    Compiler compiler(&m_engineMock);
+
+    // touching (Sprite2)
+    auto block1 = std::make_shared<Block>("a", "sensing_touchingobject");
+    addDropdownInput(block1, "TOUCHINGOBJECTMENU", SensingBlocks::TOUCHINGOBJECTMENU, "Sprite2");
+
+    // touching (mouse-pointer)
+    auto block2 = std::make_shared<Block>("b", "sensing_touchingobject");
+    addDropdownInput(block2, "TOUCHINGOBJECTMENU", SensingBlocks::TOUCHINGOBJECTMENU, "_mouse_");
+
+    // touching (edge)
+    auto block3 = std::make_shared<Block>("c", "sensing_touchingobject");
+    addDropdownInput(block3, "TOUCHINGOBJECTMENU", SensingBlocks::TOUCHINGOBJECTMENU, "_edge_");
+
+    // touching (null block)
+    auto block4 = std::make_shared<Block>("d", "sensing_distanceto");
+    addDropdownInput(block4, "TOUCHINGOBJECTMENU", SensingBlocks::TOUCHINGOBJECTMENU, "", createNullBlock("e"));
+
+    compiler.init();
+
+    EXPECT_CALL(m_engineMock, findTarget("Sprite2")).WillOnce(Return(5));
+    EXPECT_CALL(m_engineMock, functionIndex(&SensingBlocks::touchingObjectByIndex)).WillOnce(Return(0));
+    compiler.setBlock(block1);
+    SensingBlocks::compileTouchingObject(&compiler);
+
+    EXPECT_CALL(m_engineMock, functionIndex(&SensingBlocks::touchingMousePointer)).WillOnce(Return(1));
+    compiler.setBlock(block2);
+    SensingBlocks::compileTouchingObject(&compiler);
+
+    EXPECT_CALL(m_engineMock, functionIndex(&SensingBlocks::touchingEdge)).WillOnce(Return(2));
+    compiler.setBlock(block3);
+    SensingBlocks::compileTouchingObject(&compiler);
+
+    EXPECT_CALL(m_engineMock, functionIndex(&SensingBlocks::touchingObject)).WillOnce(Return(3));
+    compiler.setBlock(block4);
+    SensingBlocks::compileTouchingObject(&compiler);
+
+    compiler.end();
+
+    ASSERT_EQ(compiler.bytecode(), std::vector<unsigned int>({ vm::OP_START, vm::OP_CONST, 0, vm::OP_EXEC, 0, vm::OP_EXEC, 1, vm::OP_EXEC, 2, vm::OP_NULL, vm::OP_EXEC, 3, vm::OP_HALT }));
+    ASSERT_EQ(compiler.constValues().size(), 1);
+    ASSERT_EQ(compiler.constValues()[0].toDouble(), 5);
+}
+
+TEST_F(SensingBlocksTest, TouchingObjectImpl)
+{
+    static unsigned int bytecode1[] = { vm::OP_START, vm::OP_CONST, 0, vm::OP_EXEC, 0, vm::OP_HALT };
+    static unsigned int bytecode2[] = { vm::OP_START, vm::OP_CONST, 1, vm::OP_EXEC, 0, vm::OP_HALT };
+    static unsigned int bytecode3[] = { vm::OP_START, vm::OP_CONST, 2, vm::OP_EXEC, 0, vm::OP_HALT };
+    static unsigned int bytecode4[] = { vm::OP_START, vm::OP_CONST, 3, vm::OP_EXEC, 0, vm::OP_HALT };
+    static unsigned int bytecode5[] = { vm::OP_START, vm::OP_CONST, 4, vm::OP_EXEC, 1, vm::OP_HALT };
+    static unsigned int bytecode6[] = { vm::OP_START, vm::OP_CONST, 5, vm::OP_EXEC, 1, vm::OP_HALT };
+    static unsigned int bytecode7[] = { vm::OP_START, vm::OP_CONST, 6, vm::OP_EXEC, 1, vm::OP_HALT };
+    static unsigned int bytecode8[] = { vm::OP_START, vm::OP_EXEC, 2, vm::OP_HALT };
+    static unsigned int bytecode9[] = { vm::OP_START, vm::OP_EXEC, 3, vm::OP_HALT };
+    static BlockFunc functions[] = { &SensingBlocks::touchingObject, &SensingBlocks::touchingObjectByIndex, &SensingBlocks::touchingMousePointer, &SensingBlocks::touchingEdge };
+    static Value constValues[] = { "Sprite2", "_mouse_", "_edge_", "", 1, -1, 2 };
+
+    TargetMock target;
+    target.setEngine(&m_engineMock);
+    Sprite sprite;
+    VirtualMachine vm(&target, &m_engineMock, nullptr);
+    vm.setFunctions(functions);
+    vm.setConstValues(constValues);
+
+    // touching "Sprite2"
+    EXPECT_CALL(m_engineMock, findTarget("Sprite2")).WillOnce(Return(3));
+    EXPECT_CALL(m_engineMock, targetAt(3)).WillOnce(Return(&sprite));
+    EXPECT_CALL(target, touchingClones).WillOnce(Return(false));
+    vm.setBytecode(bytecode1);
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 1);
+    ASSERT_FALSE(vm.getInput(0, 1)->toBool());
+
+    EXPECT_CALL(m_engineMock, findTarget("Sprite2")).WillOnce(Return(3));
+    EXPECT_CALL(m_engineMock, targetAt(3)).WillOnce(Return(&sprite));
+    EXPECT_CALL(target, touchingClones).WillOnce(Return(true));
+    vm.reset();
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 1);
+    ASSERT_TRUE(vm.getInput(0, 1)->toBool());
+
+    // touching "_mouse_"
+    EXPECT_CALL(m_engineMock, mouseX()).WillOnce(Return(24.5));
+    EXPECT_CALL(m_engineMock, mouseY()).WillOnce(Return(-16.04));
+    EXPECT_CALL(target, touchingPoint(24.5, -16.04)).WillOnce(Return(true));
+    vm.setBytecode(bytecode2);
+    vm.reset();
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 1);
+    ASSERT_TRUE(vm.getInput(0, 1)->toBool());
+
+    // touching "_edge_"
+    EXPECT_CALL(m_engineMock, stageWidth()).WillOnce(Return(0));
+    EXPECT_CALL(m_engineMock, stageHeight()).WillOnce(Return(0));
+    EXPECT_CALL(target, boundingRect()).WillOnce(Return(Rect(-5, 5, 5, -5)));
+    vm.setBytecode(bytecode3);
+    vm.reset();
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 1);
+    ASSERT_TRUE(vm.getInput(0, 1)->toBool());
+
+    EXPECT_CALL(m_engineMock, stageWidth()).WillOnce(Return(10));
+    EXPECT_CALL(m_engineMock, stageHeight()).WillOnce(Return(10));
+    EXPECT_CALL(target, boundingRect()).WillOnce(Return(Rect(-5, 5, 5, -5)));
+    vm.reset();
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 1);
+    ASSERT_FALSE(vm.getInput(0, 1)->toBool());
+
+    // touching ""
+    EXPECT_CALL(m_engineMock, findTarget("")).WillOnce(Return(-1));
+    EXPECT_CALL(m_engineMock, targetAt(-1)).WillOnce(Return(nullptr));
+    vm.setBytecode(bytecode4);
+    vm.reset();
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 1);
+    ASSERT_FALSE(vm.getInput(0, 1)->toBool());
+
+    // touching Sprite2
+    EXPECT_CALL(m_engineMock, targetAt(1)).WillOnce(Return(&sprite));
+    EXPECT_CALL(target, touchingClones).WillOnce(Return(false));
+    vm.setBytecode(bytecode5);
+    vm.reset();
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 1);
+    ASSERT_FALSE(vm.getInput(0, 1)->toBool());
+
+    // touching (invalid)
+    EXPECT_CALL(m_engineMock, targetAt(-1)).WillOnce(Return(nullptr));
+    vm.setBytecode(bytecode6);
+    vm.reset();
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 1);
+    ASSERT_FALSE(vm.getInput(0, 1)->toBool());
+
+    EXPECT_CALL(m_engineMock, targetAt(2)).WillOnce(Return(nullptr));
+    vm.setBytecode(bytecode7);
+    vm.reset();
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 1);
+    ASSERT_FALSE(vm.getInput(0, 1)->toBool());
+
+    // touching mouse-pointer
+    EXPECT_CALL(m_engineMock, mouseX()).WillOnce(Return(168.087));
+    EXPECT_CALL(m_engineMock, mouseY()).WillOnce(Return(175.908));
+    EXPECT_CALL(target, touchingPoint(168.087, 175.908)).WillOnce(Return(true));
+    vm.setBytecode(bytecode8);
+    vm.reset();
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 1);
+    ASSERT_TRUE(vm.getInput(0, 1)->toBool());
+
+    // touching edge
+    EXPECT_CALL(m_engineMock, stageWidth()).WillOnce(Return(0));
+    EXPECT_CALL(m_engineMock, stageHeight()).WillOnce(Return(0));
+    EXPECT_CALL(target, boundingRect()).WillOnce(Return(Rect(-5, 5, 5, -5)));
+    vm.setBytecode(bytecode9);
+    vm.reset();
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 1);
+    ASSERT_TRUE(vm.getInput(0, 1)->toBool());
+
+    EXPECT_CALL(m_engineMock, stageWidth()).WillOnce(Return(10));
+    EXPECT_CALL(m_engineMock, stageHeight()).WillOnce(Return(10));
+    EXPECT_CALL(target, boundingRect()).WillOnce(Return(Rect(-5, 5, 5, -5)));
+    vm.reset();
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 1);
+    ASSERT_FALSE(vm.getInput(0, 1)->toBool());
+}
+
 TEST_F(SensingBlocksTest, DistanceTo)
 {
     Compiler compiler(&m_engineMock);

From f5e6e999e20f5c7eb3140402b86e762325fc98ee Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Tue, 26 Mar 2024 20:01:45 +0100
Subject: [PATCH 62/77] Handle the edge case of checking if a sprite touches
 itself

---
 src/scratch/target.cpp               |  4 ++--
 test/scratch_classes/sprite_test.cpp | 24 ++++++++++++++++++++++++
 2 files changed, 26 insertions(+), 2 deletions(-)

diff --git a/src/scratch/target.cpp b/src/scratch/target.cpp
index 8bfbd2c7..2a75d74e 100644
--- a/src/scratch/target.cpp
+++ b/src/scratch/target.cpp
@@ -443,11 +443,11 @@ bool Target::touchingSprite(Sprite *sprite) const
     assert(firstClone);
     std::vector<Sprite *> clones;
 
-    if (true) // TODO: Filter clones that are being dragged, including firstClone
+    if (firstClone != this) // TODO: Filter clones that are being dragged, including firstClone
         clones.push_back(firstClone);
 
     for (auto clone : firstClone->clones()) {
-        if (true) // TODO: Filter clones that are being dragged
+        if (clone.get() != this) // TODO: Filter clones that are being dragged
             clones.push_back(clone.get());
     }
 
diff --git a/test/scratch_classes/sprite_test.cpp b/test/scratch_classes/sprite_test.cpp
index 61b79be9..2998d900 100644
--- a/test/scratch_classes/sprite_test.cpp
+++ b/test/scratch_classes/sprite_test.cpp
@@ -649,6 +649,30 @@ TEST(SpriteTest, TouchingSprite)
 
     EXPECT_CALL(iface, touchingClones).WillOnce(Return(true));
     ASSERT_TRUE(sprite.touchingSprite(&another));
+
+    SpriteHandlerMock iface2;
+    EXPECT_CALL(iface2, init);
+    another.setInterface(&iface2);
+    clones.erase(clones.begin()); // the sprite cannot touch itself
+
+    EXPECT_CALL(iface2, touchingClones(_)).WillOnce(WithArgs<0>(Invoke([&clones, &actualClones](const std::vector<Sprite *> &candidates) {
+        actualClones = candidates;
+        return false;
+    })));
+    ASSERT_FALSE(another.touchingSprite(&another));
+    ASSERT_EQ(clones, actualClones);
+    clones.insert(clones.begin(), &another);
+
+    SpriteHandlerMock iface3;
+    EXPECT_CALL(iface3, init);
+    clone2->setInterface(&iface3);
+    clones.erase(clones.begin() + 2); // the clone cannot touch itself, but can touch the sprite (clone root)
+    EXPECT_CALL(iface3, touchingClones(_)).WillOnce(WithArgs<0>(Invoke([&clones, &actualClones](const std::vector<Sprite *> &candidates) {
+        actualClones = candidates;
+        return false;
+    })));
+    ASSERT_FALSE(clone2->touchingSprite(clone2.get()));
+    ASSERT_EQ(clones, actualClones);
 }
 
 TEST(SpriteTest, TouchingPoint)

From 70abbf27f18660b7384eff3f7a876cca0447699f Mon Sep 17 00:00:00 2001
From: Jessie YU <jy2728@nyu.edu>
Date: Tue, 26 Mar 2024 20:25:34 +0100
Subject: [PATCH 63/77] fix #479: Add bubble text length limit

---
 src/scratch/target.cpp               |  3 ++-
 test/scratch_classes/target_test.cpp | 19 +++++++++++++++++++
 2 files changed, 21 insertions(+), 1 deletion(-)

diff --git a/src/scratch/target.cpp b/src/scratch/target.cpp
index 3392bef2..e0d8ff33 100644
--- a/src/scratch/target.cpp
+++ b/src/scratch/target.cpp
@@ -466,7 +466,8 @@ const std::string &Target::bubbleText() const
  */
 void Target::setBubbleText(const std::string &text)
 {
-    impl->bubbleText = text;
+    size_t limit = 330;
+    impl->bubbleText = text.substr(0, limit);
 }
 
 /*! Returns the engine. */
diff --git a/test/scratch_classes/target_test.cpp b/test/scratch_classes/target_test.cpp
index 6924980d..2f99b90b 100644
--- a/test/scratch_classes/target_test.cpp
+++ b/test/scratch_classes/target_test.cpp
@@ -603,6 +603,25 @@ TEST(TargetTest, BubbleText)
 
     target.setBubbleText("world");
     ASSERT_EQ(target.bubbleText(), "world");
+
+    // longstr.length = 384, should be limited to 330 in bubble text
+    std::string longstr =
+        "EY8OUNzAqwgh7NRGk5TzCP3dkAhJy9TX"
+        "Y9mqKElPjdQpKddYqjyCwUk2hx6YgVZV"
+        "6BOdmZGxDMs8Hjv8W9G6j4gTxAWdOkzs"
+        "8Ih80xzEDbvLilWsDwoB6FxH2kVVI4xs"
+        "IXOETNQ6QMsCKLWc5XjHk2BS9nYvDGpJ"
+        "uEmp9zIzFGT1kRSrOlU3ZwnN1YtvqFx"
+        "3hkWVNtJ71dQ0PJHhOVQPUy19V01SPu3"
+        "KIIS2wdSUVAc4RYMzepSveghzWbdcizy"
+        "Tm1KKAj4svu9YoL8b9vsolG8gKunvKO7"
+        "MurRKSeUbECELnJEKV6683xCq7RvmjAu"
+        "2djZ54apiQc1lTixWns5GoG0SVNuFzHl"
+        "q97qUiqiMecjVFM51YVif7c1Stip52Hl";
+
+    target.setBubbleText(longstr);
+    ASSERT_EQ(target.bubbleText().length(), 330);
+    ASSERT_EQ(target.bubbleText(), longstr.substr(0, 330));
 }
 
 TEST(TargetTest, Engine)

From 353692d0fbc6422e62b590bf271b3947ad42c7b3 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Wed, 27 Mar 2024 09:08:33 +0100
Subject: [PATCH 64/77] Fix build without audio support

---
 src/audio/internal/audioloudnessstub.cpp | 6 +++---
 src/audio/internal/audioloudnessstub.h   | 4 ++--
 src/audio/internal/audioplayerstub.h     | 2 +-
 3 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/src/audio/internal/audioloudnessstub.cpp b/src/audio/internal/audioloudnessstub.cpp
index 9949a6f4..f57903ac 100644
--- a/src/audio/internal/audioloudnessstub.cpp
+++ b/src/audio/internal/audioloudnessstub.cpp
@@ -1,14 +1,14 @@
 // SPDX-License-Identifier: Apache-2.0
 
-#include "audioloudness.h"
+#include "audioloudnessstub.h"
 
 using namespace libscratchcpp;
 
-AudioLoudness::AudioLoudness()
+AudioLoudnessStub::AudioLoudnessStub()
 {
 }
 
-int AudioLoudness::getLoudness() const
+int AudioLoudnessStub::getLoudness() const
 {
     return -1;
 }
diff --git a/src/audio/internal/audioloudnessstub.h b/src/audio/internal/audioloudnessstub.h
index bd1be9fa..d8e307a3 100644
--- a/src/audio/internal/audioloudnessstub.h
+++ b/src/audio/internal/audioloudnessstub.h
@@ -7,10 +7,10 @@
 namespace libscratchcpp
 {
 
-class AudioLoudness : public IAudioLoudness
+class AudioLoudnessStub : public IAudioLoudness
 {
     public:
-        AudioLoudness();
+        AudioLoudnessStub();
 
         int getLoudness() const override;
 };
diff --git a/src/audio/internal/audioplayerstub.h b/src/audio/internal/audioplayerstub.h
index 8128240d..598e7737 100644
--- a/src/audio/internal/audioplayerstub.h
+++ b/src/audio/internal/audioplayerstub.h
@@ -2,7 +2,7 @@
 
 #pragma once
 
-#include "iaudioplayer.h"
+#include "../iaudioplayer.h"
 
 namespace libscratchcpp
 {

From 9a1d262bfcf778ddb2b24f339e9aa5ed95ba8ad0 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Wed, 27 Mar 2024 15:51:47 +0100
Subject: [PATCH 65/77] Handle invisible sprites in touchingSprite()

---
 src/scratch/sprite.cpp               |  2 +-
 src/scratch/target.cpp               |  4 ++--
 test/scratch_classes/sprite_test.cpp | 12 ++++++++++
 test/scratch_classes/target_test.cpp | 33 ++++++++++++++++++++++++++++
 4 files changed, 48 insertions(+), 3 deletions(-)

diff --git a/src/scratch/sprite.cpp b/src/scratch/sprite.cpp
index ce76a99f..fa720e57 100644
--- a/src/scratch/sprite.cpp
+++ b/src/scratch/sprite.cpp
@@ -479,7 +479,7 @@ Target *Sprite::dataSource() const
 
 bool Sprite::touchingClones(const std::vector<Sprite *> &clones) const
 {
-    if (!impl->iface)
+    if (!impl->iface || !impl->visible) // If the sprite invisible, it cannot touch anything
         return false;
 
     return impl->iface->touchingClones(clones);
diff --git a/src/scratch/target.cpp b/src/scratch/target.cpp
index d8ef3173..cd9930d1 100644
--- a/src/scratch/target.cpp
+++ b/src/scratch/target.cpp
@@ -443,11 +443,11 @@ bool Target::touchingSprite(Sprite *sprite) const
     assert(firstClone);
     std::vector<Sprite *> clones;
 
-    if (firstClone != this) // TODO: Filter clones that are being dragged, including firstClone
+    if (firstClone != this && firstClone->visible()) // TODO: Filter clones that are being dragged, including firstClone
         clones.push_back(firstClone);
 
     for (auto clone : firstClone->clones()) {
-        if (clone.get() != this) // TODO: Filter clones that are being dragged
+        if (clone.get() != this && clone->visible()) // TODO: Filter clones that are being dragged
             clones.push_back(clone.get());
     }
 
diff --git a/test/scratch_classes/sprite_test.cpp b/test/scratch_classes/sprite_test.cpp
index 2998d900..4a0eb17a 100644
--- a/test/scratch_classes/sprite_test.cpp
+++ b/test/scratch_classes/sprite_test.cpp
@@ -673,6 +673,18 @@ TEST(SpriteTest, TouchingSprite)
     })));
     ASSERT_FALSE(clone2->touchingSprite(clone2.get()));
     ASSERT_EQ(clones, actualClones);
+
+    // If the sprite is invisible, it cannot touch anything
+    EXPECT_CALL(iface, onVisibleChanged).Times(2);
+    sprite.setVisible(false);
+    EXPECT_CALL(iface, touchingClones).Times(0);
+    ASSERT_FALSE(sprite.touchingSprite(&another));
+
+    sprite.setVisible(true);
+    EXPECT_CALL(iface3, onVisibleChanged);
+    clone2->setVisible(false);
+    EXPECT_CALL(iface, touchingClones).Times(0);
+    ASSERT_FALSE(clone2->touchingSprite(&another));
 }
 
 TEST(SpriteTest, TouchingPoint)
diff --git a/test/scratch_classes/target_test.cpp b/test/scratch_classes/target_test.cpp
index 6429c5ad..7b5fcbd2 100644
--- a/test/scratch_classes/target_test.cpp
+++ b/test/scratch_classes/target_test.cpp
@@ -619,6 +619,39 @@ TEST(TargetTest, TouchingSprite)
     })));
     ASSERT_TRUE(target.touchingSprite(clone2.get()));
     ASSERT_EQ(clones, actualClones);
+
+    // Pass an invisible sprite
+    clone2->setVisible(false);
+    clones.erase(clones.begin() + 2);
+    EXPECT_CALL(target, touchingClones(_)).WillOnce(WithArgs<0>(Invoke([&clones, &actualClones](const std::vector<Sprite *> &candidates) {
+        actualClones = candidates;
+        return true;
+    })));
+    ASSERT_TRUE(target.touchingSprite(clone2.get()));
+    ASSERT_EQ(clones, actualClones);
+
+    EXPECT_CALL(target, touchingClones(_)).WillOnce(WithArgs<0>(Invoke([&clones, &actualClones](const std::vector<Sprite *> &candidates) {
+        actualClones = candidates;
+        return true;
+    })));
+    ASSERT_TRUE(target.touchingSprite(&sprite));
+    ASSERT_EQ(clones, actualClones);
+
+    sprite.setVisible(false);
+    clones.erase(clones.begin());
+    EXPECT_CALL(target, touchingClones(_)).WillOnce(WithArgs<0>(Invoke([&clones, &actualClones](const std::vector<Sprite *> &candidates) {
+        actualClones = candidates;
+        return true;
+    })));
+    ASSERT_TRUE(target.touchingSprite(clone3.get()));
+    ASSERT_EQ(clones, actualClones);
+
+    EXPECT_CALL(target, touchingClones(_)).WillOnce(WithArgs<0>(Invoke([&clones, &actualClones](const std::vector<Sprite *> &candidates) {
+        actualClones = candidates;
+        return true;
+    })));
+    ASSERT_TRUE(target.touchingSprite(&sprite));
+    ASSERT_EQ(clones, actualClones);
 }
 
 TEST(TargetTest, TouchingPoint)

From ff467df638697a8384f22a8f80b0d23cdb906afd Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Mon, 1 Apr 2024 12:11:11 +0200
Subject: [PATCH 66/77] Fix touching object opcode in sensing blocks test

---
 test/blocks/sensing_blocks_test.cpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/test/blocks/sensing_blocks_test.cpp b/test/blocks/sensing_blocks_test.cpp
index 8857c759..06189b7b 100644
--- a/test/blocks/sensing_blocks_test.cpp
+++ b/test/blocks/sensing_blocks_test.cpp
@@ -202,7 +202,7 @@ TEST_F(SensingBlocksTest, TouchingObject)
     addDropdownInput(block3, "TOUCHINGOBJECTMENU", SensingBlocks::TOUCHINGOBJECTMENU, "_edge_");
 
     // touching (null block)
-    auto block4 = std::make_shared<Block>("d", "sensing_distanceto");
+    auto block4 = std::make_shared<Block>("d", "sensing_touchingobject");
     addDropdownInput(block4, "TOUCHINGOBJECTMENU", SensingBlocks::TOUCHINGOBJECTMENU, "", createNullBlock("e"));
 
     compiler.init();

From 0353b336ae41fac4a8cf21925891f46abded02a7 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Mon, 1 Apr 2024 13:09:16 +0200
Subject: [PATCH 67/77] Add touchingColor() method

---
 include/scratchcpp/ispritehandler.h  |  3 +++
 include/scratchcpp/istagehandler.h   |  3 +++
 include/scratchcpp/sprite.h          |  1 +
 include/scratchcpp/stage.h           |  1 +
 include/scratchcpp/target.h          |  1 +
 src/scratch/sprite.cpp               |  9 +++++++++
 src/scratch/stage.cpp                |  9 +++++++++
 src/scratch/target.cpp               |  6 ++++++
 test/mocks/spritehandlermock.h       |  1 +
 test/mocks/stagehandlermock.h        |  1 +
 test/mocks/targetmock.h              |  2 ++
 test/scratch_classes/sprite_test.cpp | 17 +++++++++++++++++
 test/scratch_classes/stage_test.cpp  | 18 ++++++++++++++++++
 test/scratch_classes/target_test.cpp |  7 +++++++
 14 files changed, 79 insertions(+)

diff --git a/include/scratchcpp/ispritehandler.h b/include/scratchcpp/ispritehandler.h
index 3f8fe20b..35173c03 100644
--- a/include/scratchcpp/ispritehandler.h
+++ b/include/scratchcpp/ispritehandler.h
@@ -83,6 +83,9 @@ class LIBSCRATCHCPP_EXPORT ISpriteHandler
 
         /*! Used to check whether the sprite touches the given point (in Scratch coordinates). */
         virtual bool touchingPoint(double x, double y) const = 0;
+
+        /*! Used to check whether the sprite touches the given color. */
+        virtual bool touchingColor(const Value &color) const = 0;
 };
 
 } // namespace libscratchcpp
diff --git a/include/scratchcpp/istagehandler.h b/include/scratchcpp/istagehandler.h
index aaf146f6..31b3c7f3 100644
--- a/include/scratchcpp/istagehandler.h
+++ b/include/scratchcpp/istagehandler.h
@@ -60,6 +60,9 @@ class LIBSCRATCHCPP_EXPORT IStageHandler
 
         /*! Used to check whether the stage touches the given point (in Scratch coordinates). */
         virtual bool touchingPoint(double x, double y) const = 0;
+
+        /*! Used to check whether the stage touches the given color. */
+        virtual bool touchingColor(const Value &color) const = 0;
 };
 
 } // namespace libscratchcpp
diff --git a/include/scratchcpp/sprite.h b/include/scratchcpp/sprite.h
index 6328c9fe..558d4685 100644
--- a/include/scratchcpp/sprite.h
+++ b/include/scratchcpp/sprite.h
@@ -73,6 +73,7 @@ class LIBSCRATCHCPP_EXPORT Sprite
         void keepInFence(double newX, double newY, double *fencedX, double *fencedY) const;
 
         bool touchingPoint(double x, double y) const override;
+        bool touchingColor(const Value &color) const override;
 
         void setGraphicsEffectValue(IGraphicsEffect *effect, double value) override;
 
diff --git a/include/scratchcpp/stage.h b/include/scratchcpp/stage.h
index cae0d396..5c6b1564 100644
--- a/include/scratchcpp/stage.h
+++ b/include/scratchcpp/stage.h
@@ -52,6 +52,7 @@ class LIBSCRATCHCPP_EXPORT Stage : public Target
         Rect fastBoundingRect() const override;
 
         bool touchingPoint(double x, double y) const override;
+        bool touchingColor(const Value &color) const override;
 
         void setGraphicsEffectValue(IGraphicsEffect *effect, double value) override;
 
diff --git a/include/scratchcpp/target.h b/include/scratchcpp/target.h
index f3e65c9c..8007bf31 100644
--- a/include/scratchcpp/target.h
+++ b/include/scratchcpp/target.h
@@ -92,6 +92,7 @@ class LIBSCRATCHCPP_EXPORT Target
         bool touchingSprite(Sprite *sprite) const;
         virtual bool touchingPoint(double x, double y) const;
         bool touchingEdge() const;
+        virtual bool touchingColor(const Value &color) const;
 
         double graphicsEffectValue(IGraphicsEffect *effect) const;
         virtual void setGraphicsEffectValue(IGraphicsEffect *effect, double value);
diff --git a/src/scratch/sprite.cpp b/src/scratch/sprite.cpp
index fa720e57..b34056e0 100644
--- a/src/scratch/sprite.cpp
+++ b/src/scratch/sprite.cpp
@@ -415,6 +415,15 @@ bool Sprite::touchingPoint(double x, double y) const
     return impl->iface->touchingPoint(x, y);
 }
 
+/*! Overrides Target#touchingColor(). */
+bool Sprite::touchingColor(const Value &color) const
+{
+    if (!impl->iface)
+        return false;
+
+    return impl->iface->touchingColor(color);
+}
+
 /*! Overrides Target#setGraphicsEffectValue(). */
 void Sprite::setGraphicsEffectValue(IGraphicsEffect *effect, double value)
 {
diff --git a/src/scratch/stage.cpp b/src/scratch/stage.cpp
index d927964a..a632570e 100644
--- a/src/scratch/stage.cpp
+++ b/src/scratch/stage.cpp
@@ -158,6 +158,15 @@ bool Stage::touchingPoint(double x, double y) const
     return impl->iface->touchingPoint(x, y);
 }
 
+/*! Overrides Target#touchingColor(). */
+bool Stage::touchingColor(const Value &color) const
+{
+    if (!impl->iface)
+        return false;
+
+    return impl->iface->touchingColor(color);
+}
+
 /*! Overrides Target#setGraphicsEffectValue(). */
 void Stage::setGraphicsEffectValue(IGraphicsEffect *effect, double value)
 {
diff --git a/src/scratch/target.cpp b/src/scratch/target.cpp
index cd9930d1..284403d1 100644
--- a/src/scratch/target.cpp
+++ b/src/scratch/target.cpp
@@ -476,6 +476,12 @@ bool Target::touchingEdge() const
     return false;
 }
 
+/*! Returns true if the Target is touching the given color (RGB triplet). */
+bool Target::touchingColor(const Value &color) const
+{
+    return false;
+}
+
 /*! Returns the value of the given graphics effect. */
 double Target::graphicsEffectValue(IGraphicsEffect *effect) const
 {
diff --git a/test/mocks/spritehandlermock.h b/test/mocks/spritehandlermock.h
index bf01381a..14bb30f8 100644
--- a/test/mocks/spritehandlermock.h
+++ b/test/mocks/spritehandlermock.h
@@ -34,4 +34,5 @@ class SpriteHandlerMock : public ISpriteHandler
 
         MOCK_METHOD(bool, touchingClones, (const std::vector<Sprite *> &), (const, override));
         MOCK_METHOD(bool, touchingPoint, (double, double), (const, override));
+        MOCK_METHOD(bool, touchingColor, (const Value &), (const, override));
 };
diff --git a/test/mocks/stagehandlermock.h b/test/mocks/stagehandlermock.h
index 0f170c61..a060ac3d 100644
--- a/test/mocks/stagehandlermock.h
+++ b/test/mocks/stagehandlermock.h
@@ -25,4 +25,5 @@ class StageHandlerMock : public IStageHandler
 
         MOCK_METHOD(bool, touchingClones, (const std::vector<Sprite *> &), (const, override));
         MOCK_METHOD(bool, touchingPoint, (double, double), (const, override));
+        MOCK_METHOD(bool, touchingColor, (const Value &), (const, override));
 };
diff --git a/test/mocks/targetmock.h b/test/mocks/targetmock.h
index bcf1cef7..2ca5140d 100644
--- a/test/mocks/targetmock.h
+++ b/test/mocks/targetmock.h
@@ -18,6 +18,8 @@ class TargetMock : public Target
 
         MOCK_METHOD(bool, touchingPoint, (double, double), (const, override));
 
+        MOCK_METHOD(bool, touchingColor, (const Value &), (const, override));
+
         MOCK_METHOD(void, setGraphicsEffectValue, (IGraphicsEffect *, double), (override));
         MOCK_METHOD(void, clearGraphicsEffects, (), (override));
 
diff --git a/test/scratch_classes/sprite_test.cpp b/test/scratch_classes/sprite_test.cpp
index 4a0eb17a..53f8639d 100644
--- a/test/scratch_classes/sprite_test.cpp
+++ b/test/scratch_classes/sprite_test.cpp
@@ -703,6 +703,23 @@ TEST(SpriteTest, TouchingPoint)
     ASSERT_TRUE(sprite.touchingPoint(-12.46, 120.72));
 }
 
+TEST(SpriteTest, TouchingColor)
+{
+    Sprite sprite;
+    ASSERT_FALSE(sprite.touchingColor(0));
+
+    SpriteHandlerMock iface;
+    EXPECT_CALL(iface, init);
+    sprite.setInterface(&iface);
+
+    Value v1 = 4278228630, v2 = "#FF00FA";
+    EXPECT_CALL(iface, touchingColor(v1)).WillOnce(Return(false));
+    ASSERT_FALSE(sprite.touchingColor(v1));
+
+    EXPECT_CALL(iface, touchingColor(v2)).WillOnce(Return(true));
+    ASSERT_TRUE(sprite.touchingColor(v2));
+}
+
 TEST(SpriteTest, GraphicsEffects)
 {
     Sprite sprite;
diff --git a/test/scratch_classes/stage_test.cpp b/test/scratch_classes/stage_test.cpp
index 747395af..4418d06f 100644
--- a/test/scratch_classes/stage_test.cpp
+++ b/test/scratch_classes/stage_test.cpp
@@ -1,6 +1,7 @@
 #include <scratchcpp/stage.h>
 #include <scratchcpp/sprite.h>
 #include <scratchcpp/costume.h>
+#include <scratchcpp/value.h>
 #include <enginemock.h>
 #include <graphicseffectmock.h>
 #include <stagehandlermock.h>
@@ -195,6 +196,23 @@ TEST(StageTest, TouchingPoint)
     ASSERT_TRUE(stage.touchingPoint(-12.46, 120.72));
 }
 
+TEST(SpriteTest, TouchingColor)
+{
+    Stage stage;
+    ASSERT_FALSE(stage.touchingColor(0));
+
+    StageHandlerMock iface;
+    EXPECT_CALL(iface, init);
+    stage.setInterface(&iface);
+
+    Value v1 = 4278228630, v2 = "#FF00FA";
+    EXPECT_CALL(iface, touchingColor(v1)).WillOnce(Return(false));
+    ASSERT_FALSE(stage.touchingColor(v1));
+
+    EXPECT_CALL(iface, touchingColor(v2)).WillOnce(Return(true));
+    ASSERT_TRUE(stage.touchingColor(v2));
+}
+
 TEST(StageTest, GraphicsEffects)
 {
     Stage stage;
diff --git a/test/scratch_classes/target_test.cpp b/test/scratch_classes/target_test.cpp
index 7b5fcbd2..5a4a6665 100644
--- a/test/scratch_classes/target_test.cpp
+++ b/test/scratch_classes/target_test.cpp
@@ -702,6 +702,13 @@ TEST(TargetTest, TouchingEdge)
     ASSERT_TRUE(sprite.touchingEdge());
 }
 
+TEST(TargetTest, TouchingColor)
+{
+    Target target;
+    Value v;
+    ASSERT_FALSE(target.touchingColor(v));
+}
+
 TEST(TargetTest, GraphicsEffects)
 {
     Target target;

From 9bc869ea4ae59919f9eb6795e239553d03b2ea80 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Mon, 1 Apr 2024 13:36:20 +0200
Subject: [PATCH 68/77] Implement sensing_touchingcolor block

---
 src/blocks/sensingblocks.cpp        | 14 +++++
 src/blocks/sensingblocks.h          |  4 ++
 test/blocks/sensing_blocks_test.cpp | 82 +++++++++++++++++++++++++++++
 3 files changed, 100 insertions(+)

diff --git a/src/blocks/sensingblocks.cpp b/src/blocks/sensingblocks.cpp
index e052064d..722be78a 100644
--- a/src/blocks/sensingblocks.cpp
+++ b/src/blocks/sensingblocks.cpp
@@ -31,6 +31,7 @@ void SensingBlocks::registerBlocks(IEngine *engine)
 {
     // Blocks
     engine->addCompileFunction(this, "sensing_touchingobject", &compileTouchingObject);
+    engine->addCompileFunction(this, "sensing_touchingcolor", &compileTouchingColor);
     engine->addCompileFunction(this, "sensing_distanceto", &compileDistanceTo);
     engine->addCompileFunction(this, "sensing_askandwait", &compileAskAndWait);
     engine->addCompileFunction(this, "sensing_answer", &compileAnswer);
@@ -58,6 +59,7 @@ void SensingBlocks::registerBlocks(IEngine *engine)
 
     // Inputs
     engine->addInput(this, "TOUCHINGOBJECTMENU", TOUCHINGOBJECTMENU);
+    engine->addInput(this, "COLOR", COLOR);
     engine->addInput(this, "DISTANCETOMENU", DISTANCETOMENU);
     engine->addInput(this, "QUESTION", QUESTION);
     engine->addInput(this, "KEY_OPTION", KEY_OPTION);
@@ -116,6 +118,12 @@ void SensingBlocks::compileTouchingObject(Compiler *compiler)
     }
 }
 
+void SensingBlocks::compileTouchingColor(Compiler *compiler)
+{
+    compiler->addInput(COLOR);
+    compiler->addFunctionCall(&touchingColor);
+}
+
 void SensingBlocks::compileDistanceTo(Compiler *compiler)
 {
     Input *input = compiler->input(DISTANCETOMENU);
@@ -495,6 +503,12 @@ unsigned int SensingBlocks::touchingEdge(VirtualMachine *vm)
     return 0;
 }
 
+unsigned int SensingBlocks::touchingColor(VirtualMachine *vm)
+{
+    vm->replaceReturnValue(vm->target()->touchingColor(*vm->getInput(0, 1)), 1);
+    return 0;
+}
+
 unsigned int SensingBlocks::keyPressed(VirtualMachine *vm)
 {
     vm->replaceReturnValue(vm->engine()->keyPressed(vm->getInput(0, 1)->toString()), 1);
diff --git a/src/blocks/sensingblocks.h b/src/blocks/sensingblocks.h
index f4590607..032c0561 100644
--- a/src/blocks/sensingblocks.h
+++ b/src/blocks/sensingblocks.h
@@ -21,6 +21,7 @@ class SensingBlocks : public IBlockSection
         enum Inputs
         {
             TOUCHINGOBJECTMENU,
+            COLOR,
             DISTANCETOMENU,
             QUESTION,
             KEY_OPTION,
@@ -61,6 +62,7 @@ class SensingBlocks : public IBlockSection
         void registerBlocks(IEngine *engine) override;
 
         static void compileTouchingObject(Compiler *compiler);
+        static void compileTouchingColor(Compiler *compiler);
         static void compileDistanceTo(Compiler *compiler);
         static void compileAskAndWait(Compiler *compiler);
         static void compileAnswer(Compiler *compiler);
@@ -90,6 +92,8 @@ class SensingBlocks : public IBlockSection
         static unsigned int touchingMousePointer(VirtualMachine *vm);
         static unsigned int touchingEdge(VirtualMachine *vm);
 
+        static unsigned int touchingColor(VirtualMachine *vm);
+
         static unsigned int keyPressed(VirtualMachine *vm);
         static unsigned int mouseDown(VirtualMachine *vm);
         static unsigned int mouseX(VirtualMachine *vm);
diff --git a/test/blocks/sensing_blocks_test.cpp b/test/blocks/sensing_blocks_test.cpp
index 06189b7b..3ad92fae 100644
--- a/test/blocks/sensing_blocks_test.cpp
+++ b/test/blocks/sensing_blocks_test.cpp
@@ -54,6 +54,14 @@ class SensingBlocksTest : public testing::Test
             return block;
         }
 
+        void addValueInput(std::shared_ptr<Block> block, const std::string &name, SensingBlocks::Inputs id, const Value &value) const
+        {
+            auto input = std::make_shared<Input>(name, Input::Type::Shadow);
+            input->setPrimaryValue(value);
+            input->setInputId(id);
+            block->addInput(input);
+        }
+
         void addObscuredInput(std::shared_ptr<Block> block, const std::string &name, SensingBlocks::Inputs id, std::shared_ptr<Block> valueBlock) const
         {
             auto input = std::make_shared<Input>(name, Input::Type::ObscuredShadow);
@@ -117,6 +125,7 @@ TEST_F(SensingBlocksTest, RegisterBlocks)
 {
     // Blocks
     EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "sensing_touchingobject", &SensingBlocks::compileTouchingObject));
+    EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "sensing_touchingcolor", &SensingBlocks::compileTouchingColor));
     EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "sensing_distanceto", &SensingBlocks::compileDistanceTo));
     EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "sensing_askandwait", &SensingBlocks::compileAskAndWait));
     EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "sensing_answer", &SensingBlocks::compileAnswer));
@@ -144,6 +153,7 @@ TEST_F(SensingBlocksTest, RegisterBlocks)
 
     // Inputs
     EXPECT_CALL(m_engineMock, addInput(m_section.get(), "TOUCHINGOBJECTMENU", SensingBlocks::TOUCHINGOBJECTMENU));
+    EXPECT_CALL(m_engineMock, addInput(m_section.get(), "COLOR", SensingBlocks::COLOR));
     EXPECT_CALL(m_engineMock, addInput(m_section.get(), "DISTANCETOMENU", SensingBlocks::DISTANCETOMENU));
     EXPECT_CALL(m_engineMock, addInput(m_section.get(), "QUESTION", SensingBlocks::QUESTION));
     EXPECT_CALL(m_engineMock, addInput(m_section.get(), "KEY_OPTION", SensingBlocks::KEY_OPTION));
@@ -371,6 +381,78 @@ TEST_F(SensingBlocksTest, TouchingObjectImpl)
     ASSERT_FALSE(vm.getInput(0, 1)->toBool());
 }
 
+TEST_F(SensingBlocksTest, TouchingColor)
+{
+    Compiler compiler(&m_engineMock);
+
+    // touching color (#FFFF00)
+    auto block1 = std::make_shared<Block>("a", "sensing_touchingcolor");
+    addValueInput(block1, "COLOR", SensingBlocks::COLOR, "#FFFF00");
+
+    // touching color (null block)
+    auto block2 = std::make_shared<Block>("b", "sensing_touchingcolor");
+    addDropdownInput(block2, "COLOR", SensingBlocks::COLOR, "", createNullBlock("c"));
+
+    compiler.init();
+
+    EXPECT_CALL(m_engineMock, functionIndex(&SensingBlocks::touchingColor)).WillOnce(Return(1));
+    compiler.setBlock(block1);
+    SensingBlocks::compileTouchingColor(&compiler);
+
+    EXPECT_CALL(m_engineMock, functionIndex(&SensingBlocks::touchingColor)).WillOnce(Return(1));
+    compiler.setBlock(block2);
+    SensingBlocks::compileTouchingColor(&compiler);
+
+    compiler.end();
+
+    ASSERT_EQ(compiler.bytecode(), std::vector<unsigned int>({ vm::OP_START, vm::OP_CONST, 0, vm::OP_EXEC, 1, vm::OP_NULL, vm::OP_EXEC, 1, vm::OP_HALT }));
+    ASSERT_EQ(compiler.constValues().size(), 1);
+    ASSERT_EQ(compiler.constValues()[0].toString(), "#FFFF00");
+}
+
+TEST_F(SensingBlocksTest, TouchingColorImpl)
+{
+    static unsigned int bytecode1[] = { vm::OP_START, vm::OP_CONST, 0, vm::OP_EXEC, 0, vm::OP_HALT };
+    static unsigned int bytecode2[] = { vm::OP_START, vm::OP_CONST, 1, vm::OP_EXEC, 0, vm::OP_HALT };
+    static BlockFunc functions[] = { &SensingBlocks::touchingColor };
+    static Value constValues[] = { "#FFFF00", 1946195606 };
+
+    TargetMock target;
+    Sprite sprite;
+    VirtualMachine vm(&target, nullptr, nullptr);
+    vm.setFunctions(functions);
+    vm.setConstValues(constValues);
+
+    EXPECT_CALL(target, touchingColor(constValues[0])).WillOnce(Return(false));
+    vm.setBytecode(bytecode1);
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 1);
+    ASSERT_FALSE(vm.getInput(0, 1)->toBool());
+
+    EXPECT_CALL(target, touchingColor(constValues[0])).WillOnce(Return(true));
+    vm.reset();
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 1);
+    ASSERT_TRUE(vm.getInput(0, 1)->toBool());
+
+    EXPECT_CALL(target, touchingColor(constValues[1])).WillOnce(Return(false));
+    vm.reset();
+    vm.setBytecode(bytecode2);
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 1);
+    ASSERT_FALSE(vm.getInput(0, 1)->toBool());
+
+    EXPECT_CALL(target, touchingColor(constValues[1])).WillOnce(Return(true));
+    vm.reset();
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 1);
+    ASSERT_TRUE(vm.getInput(0, 1)->toBool());
+}
+
 TEST_F(SensingBlocksTest, DistanceTo)
 {
     Compiler compiler(&m_engineMock);

From 2290b06359402893a8440367a01b31f0a331c635 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Mon, 1 Apr 2024 22:10:26 +0200
Subject: [PATCH 69/77] Add API for sprite dragging

---
 include/scratchcpp/sprite.h          |  5 +++
 src/scratch/sprite.cpp               | 42 ++++++++++++++++++++++
 src/scratch/sprite_p.h               |  1 +
 test/scratch_classes/sprite_test.cpp | 53 ++++++++++++++++++++++++++++
 4 files changed, 101 insertions(+)

diff --git a/include/scratchcpp/sprite.h b/include/scratchcpp/sprite.h
index 558d4685..97805772 100644
--- a/include/scratchcpp/sprite.h
+++ b/include/scratchcpp/sprite.h
@@ -49,6 +49,11 @@ class LIBSCRATCHCPP_EXPORT Sprite
 
         void setPosition(double x, double y);
 
+        bool dragging() const;
+        void startDragging();
+        void stopDragging();
+        void dragToPosition(double x, double y);
+
         double size() const;
         void setSize(double newSize);
 
diff --git a/src/scratch/sprite.cpp b/src/scratch/sprite.cpp
index b34056e0..eac891bf 100644
--- a/src/scratch/sprite.cpp
+++ b/src/scratch/sprite.cpp
@@ -169,6 +169,9 @@ double Sprite::x() const
 /*! Sets the X position of the sprite. */
 void Sprite::setX(double newX)
 {
+    if (impl->dragging)
+        return;
+
     setXY(newX, impl->y);
 
     if (impl->iface)
@@ -184,6 +187,9 @@ double Sprite::y() const
 /*! Sets the Y position of the sprite. */
 void Sprite::setY(double newY)
 {
+    if (impl->dragging)
+        return;
+
     setXY(impl->x, newY);
 
     if (impl->iface)
@@ -193,6 +199,42 @@ void Sprite::setY(double newY)
 /* Sets the position of the sprite. */
 void Sprite::setPosition(double x, double y)
 {
+    if (impl->dragging)
+        return;
+
+    setXY(x, y);
+
+    if (impl->iface) {
+        impl->iface->onXChanged(impl->x);
+        impl->iface->onYChanged(impl->y);
+    }
+}
+
+/*! Returns true if the sprite is being dragged. */
+bool Sprite::dragging() const
+{
+    return impl->dragging;
+}
+
+/*! Starts dragging. Calls to setX(), setY() and setPosition() will be ignored until stopDragging() is called. */
+void Sprite::startDragging()
+{
+    impl->dragging = true;
+}
+
+/*! Stops dragging. */
+void Sprite::stopDragging()
+{
+    impl->dragging = false;
+}
+
+/*!
+ * Drags the sprite to the given position.
+ * \note If startDragging() wasn't called before calling this method, dragging will be started automatically.
+ */
+void Sprite::dragToPosition(double x, double y)
+{
+    impl->dragging = true;
     setXY(x, y);
 
     if (impl->iface) {
diff --git a/src/scratch/sprite_p.h b/src/scratch/sprite_p.h
index 5cc437f2..c2268064 100644
--- a/src/scratch/sprite_p.h
+++ b/src/scratch/sprite_p.h
@@ -30,6 +30,7 @@ struct SpritePrivate
         double size = 100;
         double direction = 90;
         bool draggable = false;
+        bool dragging = false;
         Sprite::RotationStyle rotationStyle = Sprite::RotationStyle::AllAround;
 };
 
diff --git a/test/scratch_classes/sprite_test.cpp b/test/scratch_classes/sprite_test.cpp
index 53f8639d..586b36d6 100644
--- a/test/scratch_classes/sprite_test.cpp
+++ b/test/scratch_classes/sprite_test.cpp
@@ -377,6 +377,59 @@ TEST(SpriteTest, XY)
     ASSERT_EQ(sprite.y(), -23);
 }
 
+TEST(SpriteTest, Dragging)
+{
+    Sprite sprite;
+    SpriteHandlerMock handler;
+    EXPECT_CALL(handler, init);
+    sprite.setInterface(&handler);
+    ASSERT_FALSE(sprite.dragging());
+
+    sprite.startDragging();
+    ASSERT_TRUE(sprite.dragging());
+
+    sprite.setX(10);
+    ASSERT_EQ(sprite.x(), 0);
+    sprite.setY(-56);
+    ASSERT_EQ(sprite.y(), 0);
+    sprite.setPosition(-2.8, 54.1);
+    ASSERT_EQ(sprite.x(), 0);
+    ASSERT_EQ(sprite.y(), 0);
+
+    EXPECT_CALL(handler, onMoved);
+    EXPECT_CALL(handler, onXChanged);
+    EXPECT_CALL(handler, onYChanged);
+    sprite.dragToPosition(34.2, -89.7);
+    ASSERT_EQ(sprite.x(), 34.2);
+    ASSERT_EQ(sprite.y(), -89.7);
+    ASSERT_TRUE(sprite.dragging());
+
+    sprite.stopDragging();
+    ASSERT_FALSE(sprite.dragging());
+
+    EXPECT_CALL(handler, onMoved);
+    EXPECT_CALL(handler, onXChanged);
+    EXPECT_CALL(handler, onYChanged);
+    sprite.setPosition(-2.8, 54.1);
+    ASSERT_EQ(sprite.x(), -2.8);
+    ASSERT_EQ(sprite.y(), 54.1);
+
+    EXPECT_CALL(handler, onMoved);
+    EXPECT_CALL(handler, onXChanged);
+    EXPECT_CALL(handler, onYChanged);
+    sprite.dragToPosition(-34.2, 89.7);
+    ASSERT_EQ(sprite.x(), -34.2);
+    ASSERT_EQ(sprite.y(), 89.7);
+    ASSERT_TRUE(sprite.dragging());
+
+    sprite.setPosition(-2.8, 54.1);
+    ASSERT_EQ(sprite.x(), -34.2);
+    ASSERT_EQ(sprite.y(), 89.7);
+
+    sprite.stopDragging();
+    ASSERT_FALSE(sprite.dragging());
+}
+
 TEST(SpriteTest, Size)
 {
     Sprite sprite;

From 7e14a0c7060255a7f46b57e3d9865b3e52a9f4c9 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Tue, 2 Apr 2024 11:32:24 +0200
Subject: [PATCH 70/77] Add assertions for undefined variables

---
 src/engine/compiler.cpp    | 2 ++
 src/scratch/inputvalue.cpp | 2 ++
 2 files changed, 4 insertions(+)

diff --git a/src/engine/compiler.cpp b/src/engine/compiler.cpp
index dae779d2..9f737b73 100644
--- a/src/engine/compiler.cpp
+++ b/src/engine/compiler.cpp
@@ -289,7 +289,9 @@ std::shared_ptr<Block> Compiler::inputBlock(int id) const
 /*! Returns the index of the given variable. */
 unsigned int Compiler::variableIndex(std::shared_ptr<Entity> varEntity)
 {
+    assert(varEntity);
     auto var = dynamic_cast<Variable *>(varEntity.get());
+    assert(var);
     auto it = std::find(impl->variables.begin(), impl->variables.end(), var);
     if (it != impl->variables.end())
         return it - impl->variables.begin();
diff --git a/src/scratch/inputvalue.cpp b/src/scratch/inputvalue.cpp
index f16f74b3..cb883b0d 100644
--- a/src/scratch/inputvalue.cpp
+++ b/src/scratch/inputvalue.cpp
@@ -34,10 +34,12 @@ void InputValue::compile(Compiler *compiler)
             break;
 
         case Type::Variable:
+            assert(impl->valuePtr);
             compiler->addInstruction(vm::OP_READ_VAR, { compiler->variableIndex(impl->valuePtr) });
             break;
 
         case Type::List:
+            assert(impl->valuePtr);
             compiler->addInstruction(vm::OP_READ_LIST, { compiler->listIndex(impl->valuePtr) });
             break;
 

From 588955d89ce9dc9bf57dba05857c0f870b722345 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Tue, 2 Apr 2024 17:54:30 +0200
Subject: [PATCH 71/77] fix #446: Create missing variables and lists used by
 inputs and fields

---
 src/engine/internal/engine.cpp                |  48 ++++++++++++++++--
 test/engine/engine_test.cpp                   |  41 +++++++++++++++
 ...undefined_variable_or_list_field_crash.sb3 | Bin 0 -> 1033 bytes
 ...undefined_variable_or_list_input_crash.sb3 | Bin 0 -> 1017 bytes
 4 files changed, 86 insertions(+), 3 deletions(-)
 create mode 100644 test/regtest_projects/446_undefined_variable_or_list_field_crash.sb3
 create mode 100644 test/regtest_projects/446_undefined_variable_or_list_input_crash.sb3

diff --git a/src/engine/internal/engine.cpp b/src/engine/internal/engine.cpp
index 4665d438..d2923948 100644
--- a/src/engine/internal/engine.cpp
+++ b/src/engine/internal/engine.cpp
@@ -113,20 +113,62 @@ void Engine::resolveIds()
             const auto &inputs = block->inputs();
             for (const auto &input : inputs) {
                 input->setValueBlock(getBlock(input->valueBlockId()));
+
                 if (container)
                     input->setInputId(container->resolveInput(input->name()));
-                input->primaryValue()->setValuePtr(getEntity(input->primaryValue()->valueId()));
-                input->secondaryValue()->setValuePtr(getEntity(input->primaryValue()->valueId()));
+
+                InputValue *value = input->primaryValue();
+                std::string id = value->valueId(); // no reference!
+                value->setValuePtr(getEntity(id));
+                assert(input->secondaryValue()->type() != InputValue::Type::Variable && input->secondaryValue()->type() != InputValue::Type::List); // secondary values never have a variable or list
+                input->secondaryValue()->setValuePtr(getEntity(input->secondaryValue()->valueId()));
+
+                // Add missing variables and lists
+                if (!value->valuePtr()) {
+                    if (value->type() == InputValue::Type::Variable) {
+                        assert(!id.empty());
+                        std::cout << "warning: variable " << value->value().toString() << " referenced by an input missing, creating in stage" << std::endl;
+                        auto var = std::make_shared<Variable>(id, value->value().toString());
+                        value->setValuePtr(var);
+                        stage()->addVariable(var);
+                    } else if (value->type() == InputValue::Type::List) {
+                        assert(!id.empty());
+                        std::cout << "warning: list " << value->value().toString() << " referenced by an input missing, creating in stage" << std::endl;
+                        std::shared_ptr<List> list = std::make_shared<List>(id, value->value().toString());
+                        value->setValuePtr(list);
+                        stage()->addList(list);
+                    }
+                }
             }
 
             const auto &fields = block->fields();
             for (auto field : fields) {
-                field->setValuePtr(getEntity(field->valueId()));
+                std::string id = field->valueId(); // no reference!
+                field->setValuePtr(getEntity(id));
+
                 if (container) {
                     field->setFieldId(container->resolveField(field->name()));
+
                     if (!field->valuePtr())
                         field->setSpecialValueId(container->resolveFieldValue(field->value().toString()));
                 }
+
+                // TODO: Move field information out of Engine
+                if (!field->valuePtr()) {
+                    if (field->name() == "VARIABLE") {
+                        assert(!id.empty());
+                        std::cout << "warning: variable " << field->value().toString() << " referenced by a field missing, creating in stage" << std::endl;
+                        auto var = std::make_shared<Variable>(id, field->value().toString());
+                        field->setValuePtr(var);
+                        stage()->addVariable(var);
+                    } else if (field->name() == "LIST") {
+                        assert(!id.empty());
+                        std::cout << "warning: list " << field->value().toString() << " referenced by a field missing, creating in stage" << std::endl;
+                        std::shared_ptr<List> list = std::make_shared<List>(id, field->value().toString());
+                        field->setValuePtr(list);
+                        stage()->addList(list);
+                    }
+                }
             }
 
             block->updateInputMap();
diff --git a/test/engine/engine_test.cpp b/test/engine/engine_test.cpp
index 2b0064fe..eef70124 100644
--- a/test/engine/engine_test.cpp
+++ b/test/engine/engine_test.cpp
@@ -2201,3 +2201,44 @@ TEST(EngineTest, NoCrashWhenLoadingUndefinedVariableOrListMonitor)
     ASSERT_EQ(list->id(), "7a5rAs|X2_[1APT7@B1V");
     ASSERT_TRUE(list->empty());
 }
+
+TEST(EngineTest, NoCrashWithUndefinedVariableOrList)
+{
+    // Regtest for #446
+    Project p("regtest_projects/446_undefined_variable_or_list_input_crash.sb3");
+    ASSERT_TRUE(p.load());
+
+    auto engine = p.engine();
+
+    // The undefined variable and list should now be defined
+    Stage *stage = engine->stage();
+    ASSERT_TRUE(stage);
+    ASSERT_VAR(stage, "test var");
+    auto var = GET_VAR(stage, "test var");
+    ASSERT_EQ(var->id(), "J3iHmLt:dwaR}^-fnGmG");
+    ASSERT_EQ(var->value(), Value());
+
+    ASSERT_LIST(stage, "test list");
+    auto list = GET_LIST(stage, "test list");
+    ASSERT_EQ(list->id(), "}l+w#Er5y!:*gh~5!3t%");
+    ASSERT_TRUE(list->empty());
+
+    // Test with fields
+    p.setFileName("regtest_projects/446_undefined_variable_or_list_field_crash.sb3");
+    ASSERT_TRUE(p.load());
+
+    engine = p.engine();
+
+    // The undefined variable and list should now be defined
+    stage = engine->stage();
+    ASSERT_TRUE(stage);
+    ASSERT_VAR(stage, "test var");
+    var = GET_VAR(stage, "test var");
+    ASSERT_EQ(var->id(), "J3iHmLt:dwaR}^-fnGmG");
+    ASSERT_EQ(var->value(), Value());
+
+    ASSERT_LIST(stage, "test list");
+    list = GET_LIST(stage, "test list");
+    ASSERT_EQ(list->id(), "}l+w#Er5y!:*gh~5!3t%");
+    ASSERT_TRUE(list->empty());
+}
diff --git a/test/regtest_projects/446_undefined_variable_or_list_field_crash.sb3 b/test/regtest_projects/446_undefined_variable_or_list_field_crash.sb3
new file mode 100644
index 0000000000000000000000000000000000000000..40629325ae8872a81f63893e0e5b793bb6ca75f8
GIT binary patch
diff --git a/test/regtest_projects/446_undefined_variable_or_list_input_crash.sb3 b/test/regtest_projects/446_undefined_variable_or_list_input_crash.sb3
new file mode 100644
index 0000000000000000000000000000000000000000..a9e746b273e44fd4d13657d14d9244ca28f8e755
GIT binary patch

From a69b720d9075a17f1fbb2a17d152c8df99500f65 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Wed, 3 Apr 2024 08:50:08 +0200
Subject: [PATCH 72/77] fix #517: Rewrite Value::toBool()

---
 include/scratchcpp/value.h          | 33 +++++++++++-----------
 test/scratch_classes/value_test.cpp | 44 +++++++++++++++++------------
 2 files changed, 42 insertions(+), 35 deletions(-)

diff --git a/include/scratchcpp/value.h b/include/scratchcpp/value.h
index ee9aab9c..fa53716a 100644
--- a/include/scratchcpp/value.h
+++ b/include/scratchcpp/value.h
@@ -320,23 +320,22 @@ class LIBSCRATCHCPP_EXPORT Value
         /*! Returns the boolean representation of the value. */
         bool toBool() const
         {
-            if (static_cast<int>(m_type) < 0) {
-                return false;
-            } else {
-                switch (m_type) {
-                    case Type::Bool:
-                        return m_boolValue;
-                    case Type::Integer:
-                        return m_intValue == 1 ? true : false;
-                    case Type::Double:
-                        return m_doubleValue == 1 ? true : false;
-                    case Type::String: {
-                        const std::string &str = m_stringValue;
-                        return (stringsEqual(str, "true") || str == "1");
-                    }
-                    default:
-                        return false;
-                }
+            switch (m_type) {
+                case Type::Bool:
+                    return m_boolValue;
+                case Type::Integer:
+                    return m_intValue != 0;
+                case Type::Double:
+                    return m_doubleValue != 0;
+                case Type::String:
+                    return !m_stringValue.empty() && !stringsEqual(m_stringValue, "false") && m_stringValue != "0";
+                case Type::Infinity:
+                case Type::NegativeInfinity:
+                    return true;
+                case Type::NaN:
+                    return false;
+                default:
+                    return false;
             }
         };
 
diff --git a/test/scratch_classes/value_test.cpp b/test/scratch_classes/value_test.cpp
index 9c6763c4..97ab5a36 100644
--- a/test/scratch_classes/value_test.cpp
+++ b/test/scratch_classes/value_test.cpp
@@ -1274,19 +1274,19 @@ TEST(ValueTest, ToDouble)
 TEST(ValueTest, ToBool)
 {
     Value v = 2147483647;
-    ASSERT_EQ(v.toBool(), false);
+    ASSERT_EQ(v.toBool(), true);
     v = -2147483647;
-    ASSERT_EQ(v.toBool(), false);
+    ASSERT_EQ(v.toBool(), true);
 
     v = 2.54;
-    ASSERT_EQ(v.toBool(), false);
+    ASSERT_EQ(v.toBool(), true);
     v = 2.54f;
-    ASSERT_EQ(v.toBool(), false);
+    ASSERT_EQ(v.toBool(), true);
 
     v = -2.54;
-    ASSERT_EQ(v.toBool(), false);
+    ASSERT_EQ(v.toBool(), true);
     v = -2.54f;
-    ASSERT_EQ(v.toBool(), false);
+    ASSERT_EQ(v.toBool(), true);
 
     v = false;
     ASSERT_EQ(v.toBool(), false);
@@ -1298,36 +1298,44 @@ TEST(ValueTest, ToBool)
     v = 1;
     ASSERT_EQ(v.toBool(), true);
     v = -1;
-    ASSERT_EQ(v.toBool(), false);
+    ASSERT_EQ(v.toBool(), true);
 
     v = "2147483647";
-    ASSERT_EQ(v.toBool(), false);
+    ASSERT_EQ(v.toBool(), true);
     v = "-2147483647";
-    ASSERT_EQ(v.toBool(), false);
+    ASSERT_EQ(v.toBool(), true);
 
     v = "255.625";
-    ASSERT_EQ(v.toBool(), false);
+    ASSERT_EQ(v.toBool(), true);
     v = "-255.625";
+    ASSERT_EQ(v.toBool(), true);
+
+    v = "0";
     ASSERT_EQ(v.toBool(), false);
 
     v = "false";
     ASSERT_EQ(v.toBool(), false);
     v = "true";
     ASSERT_EQ(v.toBool(), true);
+    v = "FaLsE";
+    ASSERT_EQ(v.toBool(), false);
+    v = "TrUe";
+    ASSERT_EQ(v.toBool(), true);
 
     v = "Infinity";
-    ASSERT_EQ(v.toBool(), false);
+    ASSERT_EQ(v.toBool(), true);
     v = "-Infinity";
-    ASSERT_EQ(v.toBool(), false);
+    ASSERT_EQ(v.toBool(), true);
     v = "NaN";
     ASSERT_EQ(v.toBool(), false);
 
     v = "something";
+    ASSERT_EQ(v.toBool(), true);
+    v = "";
     ASSERT_EQ(v.toBool(), false);
 
-    // TODO: Uncomment this (#517)
     // Hex
-    /*v = "0xafe";
+    v = "0xafe";
     ASSERT_TRUE(v.isString());
     ASSERT_TRUE(v.toBool());
 
@@ -1341,7 +1349,7 @@ TEST(ValueTest, ToBool)
 
     v = "0x0";
     ASSERT_TRUE(v.isString());
-    ASSERT_FALSE(v.toBool());
+    ASSERT_TRUE(v.toBool());
 
     v = "0XBaCD";
     ASSERT_TRUE(v.isString());
@@ -1349,7 +1357,7 @@ TEST(ValueTest, ToBool)
 
     v = "0xAbG";
     ASSERT_TRUE(v.isString());
-    ASSERT_FALSE(v.toBool());
+    ASSERT_TRUE(v.toBool());
 
     // Octal
     v = "0o506";
@@ -1366,7 +1374,7 @@ TEST(ValueTest, ToBool)
 
     v = "0o5783";
     ASSERT_TRUE(v.isString());
-    ASSERT_FALSE(v.toBool());
+    ASSERT_TRUE(v.toBool());
 
     // Binary
     v = "0b101101";
@@ -1383,7 +1391,7 @@ TEST(ValueTest, ToBool)
 
     v = "0b100112001";
     ASSERT_TRUE(v.isString());
-    ASSERT_FALSE(v.toBool());*/
+    ASSERT_TRUE(v.toBool());
 }
 
 TEST(ValueTest, ToString)

From e3d227c56123b87745617a2305037d67a4804e07 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Mon, 8 Apr 2024 09:18:22 +0200
Subject: [PATCH 73/77] Add cloning methods to AudioPlayer

---
 src/audio/iaudioplayer.h               |  5 ++++
 src/audio/internal/audioplayer.cpp     | 35 +++++++++++++++++++++++++-
 src/audio/internal/audioplayer.h       |  5 ++++
 src/audio/internal/audioplayerstub.cpp | 16 ++++++++++++
 src/audio/internal/audioplayerstub.h   |  8 ++++++
 test/audio/CMakeLists.txt              | 17 +++++++++++++
 test/audio/audioplayer_test.cpp        | 14 +++++++++++
 test/mocks/audioplayermock.h           |  5 ++++
 8 files changed, 104 insertions(+), 1 deletion(-)
 create mode 100644 test/audio/audioplayer_test.cpp

diff --git a/src/audio/iaudioplayer.h b/src/audio/iaudioplayer.h
index e107c854..0023f195 100644
--- a/src/audio/iaudioplayer.h
+++ b/src/audio/iaudioplayer.h
@@ -11,8 +11,13 @@ class IAudioPlayer
         virtual ~IAudioPlayer() { }
 
         virtual bool load(unsigned int size, const void *data, unsigned long sampleRate) = 0;
+        virtual bool loadCopy(IAudioPlayer *player) = 0;
+
+        virtual float volume() const = 0;
         virtual void setVolume(float volume) = 0;
 
+        virtual bool isLoaded() const = 0;
+
         virtual void start() = 0;
         virtual void stop() = 0;
 
diff --git a/src/audio/internal/audioplayer.cpp b/src/audio/internal/audioplayer.cpp
index c5b5bb8c..6ac26368 100644
--- a/src/audio/internal/audioplayer.cpp
+++ b/src/audio/internal/audioplayer.cpp
@@ -1,6 +1,7 @@
 // SPDX-License-Identifier: Apache-2.0
 
 #include <iostream>
+#include <cassert>
 
 #include "audioplayer.h"
 #include "audioengine.h"
@@ -26,7 +27,7 @@ AudioPlayer::~AudioPlayer()
 
 bool AudioPlayer::load(unsigned int size, const void *data, unsigned long sampleRate)
 {
-    if (!AudioEngine::initialized())
+    if (!AudioEngine::initialized() || m_loaded)
         return false;
 
     ma_engine *engine = AudioEngine::engine();
@@ -55,6 +56,33 @@ bool AudioPlayer::load(unsigned int size, const void *data, unsigned long sample
     return true;
 }
 
+bool AudioPlayer::loadCopy(IAudioPlayer *player)
+{
+    assert(player && dynamic_cast<AudioPlayer *>(player));
+
+    if (!AudioEngine::initialized() || !player)
+        return false;
+
+    ma_engine *engine = AudioEngine::engine();
+
+    AudioPlayer *playerPtr = static_cast<AudioPlayer *>(player);
+    ma_result initResult = ma_sound_init_from_data_source(engine, playerPtr->m_decoder, MA_SOUND_FLAG_DECODE, NULL, m_sound);
+
+    if (initResult != MA_SUCCESS) {
+        std::cerr << "Failed to init sound copy." << std::endl;
+        return false;
+    }
+
+    m_loaded = true;
+    ma_sound_set_volume(m_sound, m_volume);
+    return true;
+}
+
+float AudioPlayer::volume() const
+{
+    return m_volume;
+}
+
 void AudioPlayer::setVolume(float volume)
 {
     m_volume = volume;
@@ -65,6 +93,11 @@ void AudioPlayer::setVolume(float volume)
     ma_sound_set_volume(m_sound, volume);
 }
 
+bool AudioPlayer::isLoaded() const
+{
+    return m_loaded;
+}
+
 void AudioPlayer::start()
 {
     if (!m_loaded)
diff --git a/src/audio/internal/audioplayer.h b/src/audio/internal/audioplayer.h
index 9cb3661b..854691ae 100644
--- a/src/audio/internal/audioplayer.h
+++ b/src/audio/internal/audioplayer.h
@@ -17,8 +17,13 @@ class AudioPlayer : public IAudioPlayer
         ~AudioPlayer();
 
         bool load(unsigned int size, const void *data, unsigned long sampleRate) override;
+        bool loadCopy(IAudioPlayer *player) override;
+
+        float volume() const override;
         void setVolume(float volume) override;
 
+        bool isLoaded() const override;
+
         void start() override;
         void stop() override;
 
diff --git a/src/audio/internal/audioplayerstub.cpp b/src/audio/internal/audioplayerstub.cpp
index e1b4d413..82983f1c 100644
--- a/src/audio/internal/audioplayerstub.cpp
+++ b/src/audio/internal/audioplayerstub.cpp
@@ -13,8 +13,24 @@ bool AudioPlayerStub::load(unsigned int size, const void *data, unsigned long sa
     return true;
 }
 
+bool AudioPlayerStub::loadCopy(IAudioPlayer *player)
+{
+    return true;
+}
+
+float AudioPlayerStub::volume() const
+{
+    return m_volume;
+}
+
 void AudioPlayerStub::setVolume(float volume)
 {
+    m_volume = volume;
+}
+
+bool AudioPlayerStub::isLoaded() const
+{
+    return true;
 }
 
 void AudioPlayerStub::start()
diff --git a/src/audio/internal/audioplayerstub.h b/src/audio/internal/audioplayerstub.h
index 598e7737..a577ddb2 100644
--- a/src/audio/internal/audioplayerstub.h
+++ b/src/audio/internal/audioplayerstub.h
@@ -13,12 +13,20 @@ class AudioPlayerStub : public IAudioPlayer
         AudioPlayerStub();
 
         bool load(unsigned int size, const void *data, unsigned long sampleRate) override;
+        bool loadCopy(IAudioPlayer *player) override;
+
+        float volume() const override;
         void setVolume(float volume) override;
 
+        bool isLoaded() const override;
+
         void start() override;
         void stop() override;
 
         bool isPlaying() const override;
+
+    private:
+        float m_volume = 1;
 };
 
 } // namespace libscratchcpp
diff --git a/test/audio/CMakeLists.txt b/test/audio/CMakeLists.txt
index c6a397b6..c4aba7b4 100644
--- a/test/audio/CMakeLists.txt
+++ b/test/audio/CMakeLists.txt
@@ -33,3 +33,20 @@ gtest_discover_tests(audioinput_test)
 if (LIBSCRATCHCPP_AUDIO_SUPPORT)
     target_compile_definitions(audioinput_test PRIVATE LIBSCRATCHCPP_AUDIO_SUPPORT)
 endif()
+
+if(LIBSCRATCHCPP_AUDIO_SUPPORT)
+    # audioplayer_test
+    add_executable(
+      audioplayer_test
+      audioplayer_test.cpp
+    )
+
+    target_link_libraries(
+      audioplayer_test
+      GTest::gtest_main
+      scratchcpp
+    )
+
+    gtest_discover_tests(audioplayer_test)
+    target_compile_definitions(audioplayer_test PRIVATE LIBSCRATCHCPP_AUDIO_SUPPORT)
+endif()
diff --git a/test/audio/audioplayer_test.cpp b/test/audio/audioplayer_test.cpp
new file mode 100644
index 00000000..cd19a920
--- /dev/null
+++ b/test/audio/audioplayer_test.cpp
@@ -0,0 +1,14 @@
+#include <audio/internal/audioplayer.h>
+
+#include "../common.h"
+
+using namespace libscratchcpp;
+
+TEST(AudioPlayerTest, Volume)
+{
+    AudioPlayer player;
+    ASSERT_EQ(player.volume(), 1);
+
+    player.setVolume(0.86f);
+    ASSERT_EQ(player.volume(), 0.86f);
+}
diff --git a/test/mocks/audioplayermock.h b/test/mocks/audioplayermock.h
index d45650fc..893e6b10 100644
--- a/test/mocks/audioplayermock.h
+++ b/test/mocks/audioplayermock.h
@@ -9,8 +9,13 @@ class AudioPlayerMock : public IAudioPlayer
 {
     public:
         MOCK_METHOD(bool, load, (unsigned int, const void *, unsigned long), (override));
+        MOCK_METHOD(bool, loadCopy, (IAudioPlayer *), (override));
+
+        MOCK_METHOD(float, volume, (), (const, override));
         MOCK_METHOD(void, setVolume, (float), (override));
 
+        MOCK_METHOD(bool, isLoaded, (), (const, override));
+
         MOCK_METHOD(void, start, (), (override));
         MOCK_METHOD(void, stop, (), (override));
 

From 203a1046bbe19029c257e152fa55a6ab452ccfeb Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Mon, 8 Apr 2024 09:19:10 +0200
Subject: [PATCH 74/77] Add clone() method to Sound

---
 include/scratchcpp/sound.h |  2 ++
 src/scratch/sound.cpp      | 16 ++++++++++++++++
 test/assets/sound_test.cpp | 35 ++++++++++++++++++++++++++++++++++-
 3 files changed, 52 insertions(+), 1 deletion(-)

diff --git a/include/scratchcpp/sound.h b/include/scratchcpp/sound.h
index 26f9df25..4185ade0 100644
--- a/include/scratchcpp/sound.h
+++ b/include/scratchcpp/sound.h
@@ -32,6 +32,8 @@ class LIBSCRATCHCPP_EXPORT Sound : public Asset
 
         virtual bool isPlaying();
 
+        std::shared_ptr<Sound> clone() const;
+
     protected:
         void processData(unsigned int size, void *data) override;
 
diff --git a/src/scratch/sound.cpp b/src/scratch/sound.cpp
index 34265722..295626b2 100644
--- a/src/scratch/sound.cpp
+++ b/src/scratch/sound.cpp
@@ -62,8 +62,24 @@ bool Sound::isPlaying()
     return impl->player->isPlaying();
 }
 
+/*! Returns an independent copy of the sound which is valid for as long as the original sound exists. */
+std::shared_ptr<Sound> Sound::clone() const
+{
+    auto sound = std::make_shared<Sound>(name(), id(), dataFormat());
+    sound->setRate(rate());
+    sound->setSampleCount(sampleCount());
+    sound->impl->player->loadCopy(impl->player.get());
+    sound->impl->player->setVolume(impl->player->volume());
+    sound->setData(dataSize(), const_cast<void *>(data()));
+
+    return sound;
+}
+
 void Sound::processData(unsigned int size, void *data)
 {
+    if (impl->player->isLoaded())
+        return;
+
     if (!impl->player->load(size, data, impl->rate))
         std::cerr << "Failed to load sound " << name() << std::endl;
 }
diff --git a/test/assets/sound_test.cpp b/test/assets/sound_test.cpp
index d35a0215..3cd80665 100644
--- a/test/assets/sound_test.cpp
+++ b/test/assets/sound_test.cpp
@@ -60,7 +60,12 @@ TEST_F(SoundTest, ProcessData)
     const char *data = "abc";
     void *dataPtr = const_cast<void *>(static_cast<const void *>(data));
 
-    EXPECT_CALL(*m_player, load(3, dataPtr, 44100));
+    EXPECT_CALL(*m_player, isLoaded()).WillOnce(Return(false));
+    EXPECT_CALL(*m_player, load(3, dataPtr, 44100)).WillOnce(Return(true));
+    sound.setData(3, dataPtr);
+
+    EXPECT_CALL(*m_player, isLoaded()).WillOnce(Return(true));
+    EXPECT_CALL(*m_player, load).Times(0);
     sound.setData(3, dataPtr);
 }
 
@@ -100,3 +105,31 @@ TEST_F(SoundTest, IsPlaying)
 
     SoundPrivate::audioOutput = nullptr;
 }
+
+TEST_F(SoundTest, Clone)
+{
+    Sound sound("sound1", "a", "wav");
+    sound.setRate(44100);
+    sound.setSampleCount(10000);
+
+    const char *data = "abc";
+    void *dataPtr = const_cast<void *>(static_cast<const void *>(data));
+
+    EXPECT_CALL(*m_player, isLoaded()).WillOnce(Return(false));
+    EXPECT_CALL(*m_player, load(3, dataPtr, 44100)).WillOnce(Return(true));
+    sound.setData(3, dataPtr);
+
+    auto clonePlayer = std::make_shared<AudioPlayerMock>();
+    EXPECT_CALL(m_playerFactory, createAudioPlayer()).WillOnce(Return(clonePlayer));
+    EXPECT_CALL(*clonePlayer, loadCopy(m_player.get())).WillOnce(Return(true));
+    EXPECT_CALL(*m_player, volume()).WillOnce(Return(0.45));
+    EXPECT_CALL(*clonePlayer, setVolume(0.45));
+    EXPECT_CALL(*clonePlayer, isLoaded()).WillOnce(Return(true));
+    auto clone = sound.clone();
+    ASSERT_TRUE(clone);
+    ASSERT_EQ(clone->name(), sound.name());
+    ASSERT_EQ(clone->id(), sound.id());
+    ASSERT_EQ(clone->dataFormat(), sound.dataFormat());
+    ASSERT_EQ(clone->rate(), sound.rate());
+    ASSERT_EQ(clone->sampleCount(), sound.sampleCount());
+}

From cee985bf094bd016fbcb26cf5ca0f1f1d15cf5f9 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Mon, 8 Apr 2024 09:19:36 +0200
Subject: [PATCH 75/77] Do not read sounds from data source

---
 src/scratch/target.cpp               | 12 ------
 test/scratch_classes/target_test.cpp | 58 ----------------------------
 2 files changed, 70 deletions(-)

diff --git a/src/scratch/target.cpp b/src/scratch/target.cpp
index 284403d1..14a6f337 100644
--- a/src/scratch/target.cpp
+++ b/src/scratch/target.cpp
@@ -331,18 +331,12 @@ int Target::findCostume(const std::string &costumeName) const
 /*! Returns the list of sounds. */
 const std::vector<std::shared_ptr<Sound>> &Target::sounds() const
 {
-    if (Target *source = dataSource())
-        return source->sounds();
-
     return impl->sounds;
 }
 
 /*! Adds a sound and returns its index. */
 int Target::addSound(std::shared_ptr<Sound> sound)
 {
-    if (Target *source = dataSource())
-        return source->addSound(sound);
-
     auto it = std::find(impl->sounds.begin(), impl->sounds.end(), sound);
 
     if (it != impl->sounds.end())
@@ -360,9 +354,6 @@ int Target::addSound(std::shared_ptr<Sound> sound)
 /*! Returns the sound at index. */
 std::shared_ptr<Sound> Target::soundAt(int index) const
 {
-    if (Target *source = dataSource())
-        return source->soundAt(index);
-
     if (index < 0 || index >= impl->sounds.size())
         return nullptr;
 
@@ -372,9 +363,6 @@ std::shared_ptr<Sound> Target::soundAt(int index) const
 /*! Returns the index of the sound with the given name. */
 int Target::findSound(const std::string &soundName) const
 {
-    if (Target *source = dataSource())
-        return source->findSound(soundName);
-
     auto it = std::find_if(impl->sounds.begin(), impl->sounds.end(), [soundName](std::shared_ptr<Sound> sound) { return sound->name() == soundName; });
 
     if (it == impl->sounds.end())
diff --git a/test/scratch_classes/target_test.cpp b/test/scratch_classes/target_test.cpp
index 5a4a6665..7c521da5 100644
--- a/test/scratch_classes/target_test.cpp
+++ b/test/scratch_classes/target_test.cpp
@@ -422,8 +422,6 @@ TEST(TargetTest, Sounds)
     auto s3 = std::make_shared<Sound>("sound3", "", "mp3");
 
     TargetMock target;
-    EXPECT_CALL(target, dataSource()).Times(14).WillRepeatedly(Return(nullptr));
-
     EXPECT_CALL(*player1, setVolume(1));
     EXPECT_CALL(*player2, setVolume(1));
     EXPECT_CALL(*player3, setVolume(1));
@@ -444,62 +442,6 @@ TEST(TargetTest, Sounds)
     ASSERT_EQ(target.findSound("sound2"), 1);
     ASSERT_EQ(target.findSound("sound3"), 2);
 
-    // Test with custom data source
-    Target source;
-
-    EXPECT_CALL(target, dataSource()).WillOnce(Return(&source));
-
-    ASSERT_TRUE(target.sounds().empty());
-
-    TargetMock target2;
-    EXPECT_CALL(target2, dataSource()).Times(15).WillRepeatedly(Return(&source));
-
-    EXPECT_CALL(*player1, setVolume(1));
-    EXPECT_CALL(*player2, setVolume(1));
-    EXPECT_CALL(*player3, setVolume(1));
-    ASSERT_EQ(target2.addSound(s1), 0);
-    ASSERT_EQ(target2.addSound(s2), 1);
-    ASSERT_EQ(target2.addSound(s3), 2);
-    ASSERT_EQ(target2.addSound(s2), 1); // add existing Sound
-
-    ASSERT_EQ(target2.sounds(), std::vector<std::shared_ptr<Sound>>({ s1, s2, s3 }));
-    ASSERT_EQ(target2.soundAt(0), s1);
-    ASSERT_EQ(target2.soundAt(1), s2);
-    ASSERT_EQ(target2.soundAt(2), s3);
-    ASSERT_EQ(target2.soundAt(3), nullptr);
-    ASSERT_EQ(target2.soundAt(-1), nullptr);
-
-    ASSERT_EQ(target2.findSound("invalid"), -1);
-    ASSERT_EQ(target2.findSound("sound1"), 0);
-    ASSERT_EQ(target2.findSound("sound2"), 1);
-    ASSERT_EQ(target2.findSound("sound3"), 2);
-
-    ASSERT_EQ(target2.sounds(), source.sounds());
-
-    auto player4 = std::make_shared<AudioPlayerMock>();
-    EXPECT_CALL(factory, createAudioPlayer()).WillOnce(Return(player4));
-    auto s4 = std::make_shared<Sound>("sound4", "", "wav");
-
-    EXPECT_CALL(*player4, setVolume(1));
-    ASSERT_EQ(source.addSound(s4), 3);
-
-    EXPECT_CALL(target2, dataSource()).WillOnce(Return(&source));
-    ASSERT_EQ(target2.sounds(), source.sounds());
-
-    ASSERT_EQ(source.sounds(), std::vector<std::shared_ptr<Sound>>({ s1, s2, s3, s4 }));
-    ASSERT_EQ(source.soundAt(0), s1);
-    ASSERT_EQ(source.soundAt(1), s2);
-    ASSERT_EQ(source.soundAt(2), s3);
-    ASSERT_EQ(source.soundAt(3), s4);
-    ASSERT_EQ(source.soundAt(4), nullptr);
-    ASSERT_EQ(source.soundAt(-1), nullptr);
-
-    ASSERT_EQ(source.findSound("invalid"), -1);
-    ASSERT_EQ(source.findSound("sound1"), 0);
-    ASSERT_EQ(source.findSound("sound2"), 1);
-    ASSERT_EQ(source.findSound("sound3"), 2);
-    ASSERT_EQ(source.findSound("sound4"), 3);
-
     SoundPrivate::audioOutput = nullptr;
 }
 

From e95b0060ccf26d42b63b2b4aeb68936143663fd6 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Mon, 8 Apr 2024 09:23:53 +0200
Subject: [PATCH 76/77] Implement sound cloning

---
 src/scratch/sprite.cpp               |  6 ++++++
 test/engine/engine_test.cpp          |  3 +++
 test/scratch_classes/sprite_test.cpp | 15 +++++++++++++++
 3 files changed, 24 insertions(+)

diff --git a/src/scratch/sprite.cpp b/src/scratch/sprite.cpp
index eac891bf..8e9845fa 100644
--- a/src/scratch/sprite.cpp
+++ b/src/scratch/sprite.cpp
@@ -6,6 +6,7 @@
 #include <scratchcpp/variable.h>
 #include <scratchcpp/list.h>
 #include <scratchcpp/costume.h>
+#include <scratchcpp/sound.h>
 #include <scratchcpp/rect.h>
 #include <cassert>
 #include <iostream>
@@ -64,6 +65,11 @@ std::shared_ptr<Sprite> Sprite::clone()
         for (auto list : l)
             clone->addList(list->clone());
 
+        const auto &sounds = this->sounds();
+
+        for (auto sound : sounds)
+            clone->addSound(sound->clone());
+
         clone->setCostumeIndex(costumeIndex());
         clone->setLayerOrder(layerOrder());
         clone->setVolume(volume());
diff --git a/test/engine/engine_test.cpp b/test/engine/engine_test.cpp
index eef70124..5b8b6f96 100644
--- a/test/engine/engine_test.cpp
+++ b/test/engine/engine_test.cpp
@@ -261,6 +261,9 @@ TEST(EngineTest, StopSounds)
     auto player3 = std::make_shared<AudioPlayerMock>();
     SoundPrivate::audioOutput = &factory;
     EXPECT_CALL(factory, createAudioPlayer()).WillOnce(Return(player1)).WillOnce(Return(player2)).WillOnce(Return(player3));
+    EXPECT_CALL(*player1, isLoaded).WillOnce(Return(false));
+    EXPECT_CALL(*player2, isLoaded).WillOnce(Return(false));
+    EXPECT_CALL(*player3, isLoaded).WillOnce(Return(false));
     EXPECT_CALL(*player1, load).WillOnce(Return(true));
     EXPECT_CALL(*player2, load).WillOnce(Return(true));
     EXPECT_CALL(*player3, load).WillOnce(Return(true));
diff --git a/test/scratch_classes/sprite_test.cpp b/test/scratch_classes/sprite_test.cpp
index 586b36d6..c2f11ed9 100644
--- a/test/scratch_classes/sprite_test.cpp
+++ b/test/scratch_classes/sprite_test.cpp
@@ -3,6 +3,7 @@
 #include <scratchcpp/variable.h>
 #include <scratchcpp/list.h>
 #include <scratchcpp/costume.h>
+#include <scratchcpp/sound.h>
 #include <scratchcpp/rect.h>
 #include <scratchcpp/project.h>
 #include <enginemock.h>
@@ -63,6 +64,12 @@ TEST(SpriteTest, Clone)
     sprite.addVariable(var2);
     auto c1 = std::make_shared<Costume>("costume1", "", "svg");
     sprite.addCostume(c1);
+    auto s1 = std::make_shared<Sound>("sound1", "a", "wav");
+    auto s2 = std::make_shared<Sound>("sound2", "b", "wav");
+    auto s3 = std::make_shared<Sound>("sound3", "c", "wav");
+    sprite.addSound(s1);
+    sprite.addSound(s2);
+    sprite.addSound(s3);
 
     auto list1 = std::make_shared<List>("c", "list1");
     list1->push_back("item1");
@@ -111,6 +118,14 @@ TEST(SpriteTest, Clone)
         ASSERT_EQ(*clone->listAt(1), std::deque<Value>({ "test" }));
         ASSERT_EQ(clone->listAt(1)->target(), clone);
 
+        ASSERT_EQ(clone->sounds().size(), 3);
+        ASSERT_EQ(clone->soundAt(0)->id(), "a");
+        ASSERT_EQ(clone->soundAt(0)->name(), "sound1");
+        ASSERT_EQ(clone->soundAt(0)->dataFormat(), "wav");
+        ASSERT_EQ(clone->soundAt(2)->id(), "c");
+        ASSERT_EQ(clone->soundAt(2)->name(), "sound3");
+        ASSERT_EQ(clone->soundAt(2)->dataFormat(), "wav");
+
         ASSERT_EQ(clone->costumeIndex(), 1);
         ASSERT_EQ(clone->layerOrder(), 5);
         ASSERT_EQ(clone->volume(), 50);

From 04baedf6f906905b22939627ce54a88ea4184a2a Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Mon, 8 Apr 2024 09:26:18 +0200
Subject: [PATCH 77/77] AudioPlayer: Ignore sounds which are not loaded in
 loadCopy()

---
 src/audio/internal/audioplayer.cpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/audio/internal/audioplayer.cpp b/src/audio/internal/audioplayer.cpp
index 6ac26368..9cab04ba 100644
--- a/src/audio/internal/audioplayer.cpp
+++ b/src/audio/internal/audioplayer.cpp
@@ -60,7 +60,7 @@ bool AudioPlayer::loadCopy(IAudioPlayer *player)
 {
     assert(player && dynamic_cast<AudioPlayer *>(player));
 
-    if (!AudioEngine::initialized() || !player)
+    if (!AudioEngine::initialized() || !player || !player->isLoaded())
         return false;
 
     ma_engine *engine = AudioEngine::engine();
