From 7c6bf999340eb4422bd0193a7e30363e0310c000 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Sat, 8 Feb 2025 10:42:43 +0100
Subject: [PATCH 01/15] Set version to 0.13.2

---
 CMakeLists.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/CMakeLists.txt b/CMakeLists.txt
index f15bf70a0..8eff2a4a6 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,6 +1,6 @@
 cmake_minimum_required(VERSION 3.14)
 
-project(libscratchcpp VERSION 0.13.1 LANGUAGES C CXX)
+project(libscratchcpp VERSION 0.13.2 LANGUAGES C CXX)
 
 set(CMAKE_INCLUDE_CURRENT_DIR ON)
 set(CMAKE_CXX_STANDARD 17)

From cc8d45c9fa1618c07b9955593f6007f333190aef Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Sat, 8 Feb 2025 10:43:12 +0100
Subject: [PATCH 02/15] Add missing clocale include to value_functions.cpp

---
 src/scratch/value_functions.cpp | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/scratch/value_functions.cpp b/src/scratch/value_functions.cpp
index fb06b5190..3d0f3c8a1 100644
--- a/src/scratch/value_functions.cpp
+++ b/src/scratch/value_functions.cpp
@@ -1,5 +1,6 @@
 #include <ctgmath>
 #include <cassert>
+#include <clocale>
 #include <utf8.h>
 
 #include "value_functions_p.h"

From 962abe22c6ffd9e6b26c9e5e696d2b75f22a76e9 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Sun, 23 Feb 2025 10:59:20 +0100
Subject: [PATCH 03/15] Revert "audio: Fix a race condition in miniaudio"

This reverts commit e60f7c071fdfb035eb9525495c15fed77db31945.
---
 .../internal/thirdparty/miniaudio/miniaudio.c   | 17 ++++++-----------
 1 file changed, 6 insertions(+), 11 deletions(-)

diff --git a/src/audio/internal/thirdparty/miniaudio/miniaudio.c b/src/audio/internal/thirdparty/miniaudio/miniaudio.c
index 077c396c4..651843b24 100644
--- a/src/audio/internal/thirdparty/miniaudio/miniaudio.c
+++ b/src/audio/internal/thirdparty/miniaudio/miniaudio.c
@@ -18616,20 +18616,15 @@ static ma_pa_buffer_attr ma_device__pa_buffer_attr_new(ma_uint32 periodSizeInFra
 static ma_pa_stream* ma_device__pa_stream_new__pulse(ma_device* pDevice, const char* pStreamName, const ma_pa_sample_spec* ss, const ma_pa_channel_map* cmap)
 {
     static int g_StreamCounter = 0;
-    static ma_mutex g_StreamCounterMutex;
     char actualStreamName[256];
 
-    ma_mutex_lock(&g_StreamCounterMutex);
-    {
-        if (pStreamName != NULL) {
-            ma_strncpy_s(actualStreamName, sizeof(actualStreamName), pStreamName, (size_t)-1);
-        } else {
-            ma_strcpy_s(actualStreamName, sizeof(actualStreamName), "miniaudio:");
-            ma_itoa_s(g_StreamCounter, actualStreamName + 8, sizeof(actualStreamName)-8, 10);  /* 8 = strlen("miniaudio:") */
-        }
-        g_StreamCounter += 1;
+    if (pStreamName != NULL) {
+        ma_strncpy_s(actualStreamName, sizeof(actualStreamName), pStreamName, (size_t)-1);
+    } else {
+        ma_strcpy_s(actualStreamName, sizeof(actualStreamName), "miniaudio:");
+        ma_itoa_s(g_StreamCounter, actualStreamName + 8, sizeof(actualStreamName)-8, 10);  /* 8 = strlen("miniaudio:") */
     }
-    ma_mutex_unlock(&g_StreamCounterMutex);
+    g_StreamCounter += 1;
 
     return ((ma_pa_stream_new_proc)pDevice->pContext->pulse.pa_stream_new)((ma_pa_context*)pDevice->pulse.pPulseContext, actualStreamName, ss, cmap);
 }

From fdff546c43a73c1f2871b3dfe786bfa6f5bc7713 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Sun, 23 Feb 2025 11:10:19 +0100
Subject: [PATCH 04/15] Build shared library of miniaudio

https://github.com/mackron/miniaudio/issues/932
---
 src/audio/CMakeLists.txt | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/src/audio/CMakeLists.txt b/src/audio/CMakeLists.txt
index 536c266bc..f3319207d 100644
--- a/src/audio/CMakeLists.txt
+++ b/src/audio/CMakeLists.txt
@@ -4,11 +4,13 @@ add_library(scratchcpp-audio STATIC)
 set(MINIAUDIO_SRC internal/thirdparty/miniaudio)
 
 if (LIBSCRATCHCPP_AUDIO_SUPPORT)
-    add_library(miniaudio STATIC
+    add_library(miniaudio SHARED
 	    ${MINIAUDIO_SRC}/miniaudio.c
 	    ${MINIAUDIO_SRC}/miniaudio.h
     )
     target_include_directories(scratchcpp-audio PUBLIC ${CMAKE_CURRENT_LIST_DIR}/${MINIAUDIO_SRC})
+    install(TARGETS miniaudio DESTINATION lib)
+
     target_link_libraries(scratchcpp-audio PRIVATE miniaudio)
     target_compile_definitions(scratchcpp-audio PUBLIC LIBSCRATCHCPP_AUDIO_SUPPORT)
 endif()

From 0fbb679c012f5c62a7fbc5d388e20601ba1cb2ed Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Mon, 24 Feb 2025 15:46:35 +0100
Subject: [PATCH 05/15] Deallocate asset data

---
 include/scratchcpp/asset.h |  2 +-
 src/project_p.cpp          |  4 +++-
 src/scratch/asset.cpp      | 14 +++++++++++++-
 src/scratch/asset_p.h      |  2 +-
 test/assets/asset_test.cpp | 14 +++++++++++++-
 test/assets/sound_test.cpp | 22 +++++++++++++---------
 6 files changed, 44 insertions(+), 14 deletions(-)

diff --git a/include/scratchcpp/asset.h b/include/scratchcpp/asset.h
index c5c10d88d..03be48043 100644
--- a/include/scratchcpp/asset.h
+++ b/include/scratchcpp/asset.h
@@ -19,7 +19,7 @@ class LIBSCRATCHCPP_EXPORT Asset : public Entity
         Asset(const std::string &name, const std::string &id, const std::string &format);
         Asset(const Asset &) = delete;
 
-        virtual ~Asset() { }
+        virtual ~Asset();
 
         void setId(const std::string &id);
 
diff --git a/src/project_p.cpp b/src/project_p.cpp
index c8c40e204..5dd383311 100644
--- a/src/project_p.cpp
+++ b/src/project_p.cpp
@@ -121,7 +121,9 @@ bool ProjectPrivate::tryLoad(IProjectReader *reader)
         // Load asset data
         for (size_t i = 0; i < assets.size(); i++) {
             const std::string &data = assetData[i];
-            assets[assetNames[i]]->setData(data.size(), static_cast<void *>(const_cast<char *>(data.c_str())));
+            char *ptr = (char *)malloc(data.size() * sizeof(char));
+            strncpy(ptr, data.data(), data.size());
+            assets[assetNames[i]]->setData(data.size(), ptr);
         }
 
     } else {
diff --git a/src/scratch/asset.cpp b/src/scratch/asset.cpp
index 79ffd4b1c..1731a828f 100644
--- a/src/scratch/asset.cpp
+++ b/src/scratch/asset.cpp
@@ -14,6 +14,15 @@ Asset::Asset(const std::string &name, const std::string &id, const std::string &
     impl->updateFileName(id);
 }
 
+/*! Destroys Asset. */
+Asset::~Asset()
+{
+    if (impl->data) {
+        free(impl->data);
+        impl->data = nullptr;
+    }
+}
+
 /*! Sets the ID (MD5 hash) of the asset file. */
 void Asset::setId(const std::string &id)
 {
@@ -51,9 +60,12 @@ unsigned int Asset::dataSize() const
     return impl->dataSize;
 }
 
-/*! Sets the asset data. */
+/*! Sets the asset data (will be deallocated when the object is destroyed). */
 void Asset::setData(unsigned int size, void *data)
 {
+    if (impl->data)
+        free(impl->data);
+
     impl->dataSize = size;
     impl->data = data;
     processData(size, data);
diff --git a/src/scratch/asset_p.h b/src/scratch/asset_p.h
index d004fb2f4..acfa4e863 100644
--- a/src/scratch/asset_p.h
+++ b/src/scratch/asset_p.h
@@ -19,7 +19,7 @@ struct AssetPrivate
         std::string name;
         std::string dataFormat;
         std::string fileName;
-        const void *data = nullptr;
+        void *data = nullptr;
         unsigned int dataSize = 0;
         Target *target = nullptr;
 };
diff --git a/test/assets/asset_test.cpp b/test/assets/asset_test.cpp
index 5bd5920be..86dcfd84c 100644
--- a/test/assets/asset_test.cpp
+++ b/test/assets/asset_test.cpp
@@ -29,12 +29,24 @@ TEST(AssetTest, Data)
     TestAsset asset;
     ASSERT_EQ(asset.data(), nullptr);
 
-    static char data[5] = "abcd";
+    char *data = (char *)malloc(4 * sizeof(char));
+    strncpy(data, "abcd", 4);
+
     asset.setData(5, data);
     ASSERT_EQ(asset.data(), data);
     ASSERT_EQ(asset.size, 5);
     ASSERT_EQ(asset.processedData, data);
     ASSERT_EQ(asset.callCount, 1);
+
+    // Should deallocate in setData()
+    data = (char *)malloc(11 * sizeof(char));
+    strncpy(data, "Hello world!", 11);
+
+    asset.setData(5, data);
+    ASSERT_EQ(asset.data(), data);
+    ASSERT_EQ(asset.size, 5);
+    ASSERT_EQ(asset.processedData, data);
+    ASSERT_EQ(asset.callCount, 2);
 }
 
 TEST(AssetTest, Target)
diff --git a/test/assets/sound_test.cpp b/test/assets/sound_test.cpp
index d3b81bf2a..b83d5a999 100644
--- a/test/assets/sound_test.cpp
+++ b/test/assets/sound_test.cpp
@@ -59,16 +59,20 @@ TEST_F(SoundTest, ProcessData)
     Sound sound("sound1", "a", "wav");
     sound.setRate(44100);
 
-    const char *data = "abc";
-    void *dataPtr = const_cast<void *>(static_cast<const void *>(data));
+    char *data = (char *)malloc(4 * sizeof(char));
+    strncpy(data, "abcd", 4);
 
     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, load(3, data, 44100)).WillOnce(Return(true));
+    sound.setData(3, data);
+
+    // Should deallocate in setData()
+    data = (char *)malloc(11 * sizeof(char));
+    strncpy(data, "Hello world!", 11);
 
     EXPECT_CALL(*m_player, isLoaded()).WillOnce(Return(true));
     EXPECT_CALL(*m_player, load).Times(0);
-    sound.setData(3, dataPtr);
+    sound.setData(3, data);
 }
 
 TEST_F(SoundTest, SetVolume)
@@ -151,12 +155,12 @@ TEST_F(SoundTest, Clone)
     sound->setRate(44100);
     sound->setSampleCount(10000);
 
-    const char *data = "abc";
-    void *dataPtr = const_cast<void *>(static_cast<const void *>(data));
+    char *data = (char *)malloc(4 * sizeof(char));
+    strncpy(data, "abcd", 4);
 
     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, load(3, data, 44100)).WillOnce(Return(true));
+    sound->setData(3, data);
 
     auto clonePlayer = std::make_shared<AudioPlayerMock>();
     EXPECT_CALL(m_playerFactory, createAudioPlayer()).WillOnce(Return(clonePlayer));

From b7f02623a94e4cc091818678be64fb7c014cc7e1 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Mon, 24 Feb 2025 16:04:00 +0100
Subject: [PATCH 06/15] ZipReader: Fix output when m_zip is null

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

diff --git a/src/internal/zipreader.cpp b/src/internal/zipreader.cpp
index fd8a48db6..f4ee39a4e 100644
--- a/src/internal/zipreader.cpp
+++ b/src/internal/zipreader.cpp
@@ -36,7 +36,7 @@ void ZipReader::close()
 size_t ZipReader::readFile(const std::string &fileName, void **buf)
 {
     if (!m_zip) {
-        buf = nullptr;
+        *buf = nullptr;
         return 0;
     }
 

From eba75086a1d96b9855ad868bd210721769a3b8b9 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Mon, 24 Feb 2025 16:04:30 +0100
Subject: [PATCH 07/15] Do not copy project.json to string when loading
 projects

---
 src/internal/scratch3reader.cpp | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/src/internal/scratch3reader.cpp b/src/internal/scratch3reader.cpp
index 7e9033266..1d7168dce 100644
--- a/src/internal/scratch3reader.cpp
+++ b/src/internal/scratch3reader.cpp
@@ -562,9 +562,11 @@ void Scratch3Reader::read()
     if (m_zipReader->open()) {
         // Parse the JSON
         try {
-            std::string jsonStr;
-            m_zipReader->readFileToString("project.json", jsonStr);
-            m_json = json::parse(jsonStr);
+            char *buf;
+            const size_t size = m_zipReader->readFile("project.json", (void **)&buf);
+            assert(buf);
+            m_json = json::parse(buf, buf + size);
+            free(buf);
         } catch (std::exception &e) {
             printErr("invalid JSON file", e.what());
         }

From 52e165e94864d953bb11e3318048b9bf2d85d171 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Mon, 24 Feb 2025 20:15:20 +0100
Subject: [PATCH 08/15] Do not free cloned asset data

---
 include/scratchcpp/asset.h | 1 +
 include/scratchcpp/sound.h | 1 +
 src/scratch/asset.cpp      | 3 ++-
 src/scratch/asset_p.h      | 1 +
 src/scratch/sound.cpp      | 5 +++++
 5 files changed, 10 insertions(+), 1 deletion(-)

diff --git a/include/scratchcpp/asset.h b/include/scratchcpp/asset.h
index 03be48043..9870c03a4 100644
--- a/include/scratchcpp/asset.h
+++ b/include/scratchcpp/asset.h
@@ -38,6 +38,7 @@ class LIBSCRATCHCPP_EXPORT Asset : public Entity
 
     protected:
         virtual void processData(unsigned int size, void *data) { }
+        virtual bool isClone() const { return false; }
 
     private:
         spimpl::unique_impl_ptr<AssetPrivate> impl;
diff --git a/include/scratchcpp/sound.h b/include/scratchcpp/sound.h
index b4906b1ee..2c95d32d2 100644
--- a/include/scratchcpp/sound.h
+++ b/include/scratchcpp/sound.h
@@ -43,6 +43,7 @@ class LIBSCRATCHCPP_EXPORT Sound : public Asset
 
     protected:
         void processData(unsigned int size, void *data) override;
+        virtual bool isClone() const override;
 
     private:
         void stopCloneSounds();
diff --git a/src/scratch/asset.cpp b/src/scratch/asset.cpp
index 1731a828f..d17b68bdf 100644
--- a/src/scratch/asset.cpp
+++ b/src/scratch/asset.cpp
@@ -17,7 +17,7 @@ Asset::Asset(const std::string &name, const std::string &id, const std::string &
 /*! Destroys Asset. */
 Asset::~Asset()
 {
-    if (impl->data) {
+    if (impl->data && !impl->dataCloned) {
         free(impl->data);
         impl->data = nullptr;
     }
@@ -68,6 +68,7 @@ void Asset::setData(unsigned int size, void *data)
 
     impl->dataSize = size;
     impl->data = data;
+    impl->dataCloned = isClone();
     processData(size, data);
 }
 
diff --git a/src/scratch/asset_p.h b/src/scratch/asset_p.h
index acfa4e863..604eeb1e6 100644
--- a/src/scratch/asset_p.h
+++ b/src/scratch/asset_p.h
@@ -21,6 +21,7 @@ struct AssetPrivate
         std::string fileName;
         void *data = nullptr;
         unsigned int dataSize = 0;
+        bool dataCloned = false;
         Target *target = nullptr;
 };
 
diff --git a/src/scratch/sound.cpp b/src/scratch/sound.cpp
index 64fcf35a0..cd3db865e 100644
--- a/src/scratch/sound.cpp
+++ b/src/scratch/sound.cpp
@@ -116,6 +116,11 @@ void Sound::processData(unsigned int size, void *data)
         std::cerr << "Failed to load sound " << name() << std::endl;
 }
 
+bool Sound::isClone() const
+{
+    return impl->cloneRoot;
+}
+
 void Sound::stopCloneSounds()
 {
     Target *target = this->target();

From a0cd2ad5ffecb4c60d171e723b3747c489d62721 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Sun, 2 Mar 2025 11:23:32 +0100
Subject: [PATCH 09/15] Fix incorrect test case name in Drawable test

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

diff --git a/test/scratch_classes/drawable_test.cpp b/test/scratch_classes/drawable_test.cpp
index 407af05d1..86c227ef1 100644
--- a/test/scratch_classes/drawable_test.cpp
+++ b/test/scratch_classes/drawable_test.cpp
@@ -40,7 +40,7 @@ TEST(DrawableTest, LayerOrder)
     ASSERT_EQ(drawable.layerOrder(), 0);
 }
 
-TEST(TargetTest, Engine)
+TEST(DrawableTest, Engine)
 {
     Drawable drawable;
     ASSERT_EQ(drawable.engine(), nullptr);

From a8c2b35fab40a2c2063c2a3f62423a7cba912571 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Sun, 2 Mar 2025 11:58:55 +0100
Subject: [PATCH 10/15] Engine: Remove duplicate event loop cleanup

---
 src/engine/internal/engine.cpp | 16 +++++-----------
 src/engine/internal/engine.h   |  1 -
 2 files changed, 5 insertions(+), 12 deletions(-)

diff --git a/src/engine/internal/engine.cpp b/src/engine/internal/engine.cpp
index c0bd3173d..5f0da4bd7 100644
--- a/src/engine/internal/engine.cpp
+++ b/src/engine/internal/engine.cpp
@@ -636,7 +636,6 @@ void Engine::run()
 {
     start();
     eventLoop(true);
-    finalize();
 }
 
 void Engine::runEventLoop()
@@ -777,7 +776,11 @@ void Engine::eventLoop(bool untilProjectStops)
             m_clock->sleep(sleepTime);
     }
 
-    finalize();
+    m_eventLoopMutex.lock();
+    m_threads.clear();
+    m_running = false;
+    m_redrawRequested = false;
+    m_eventLoopMutex.unlock();
 }
 
 bool Engine::isRunning() const
@@ -1962,15 +1965,6 @@ void Engine::compileMonitor(std::shared_ptr<Monitor> monitor)
 #endif // USE_LLVM
 }
 
-void Engine::finalize()
-{
-    m_eventLoopMutex.lock();
-    m_threads.clear();
-    m_running = false;
-    m_redrawRequested = false;
-    m_eventLoopMutex.unlock();
-}
-
 void Engine::deleteClones()
 {
     m_eventLoopMutex.lock();
diff --git a/src/engine/internal/engine.h b/src/engine/internal/engine.h
index ed4b562dd..33cd172c6 100644
--- a/src/engine/internal/engine.h
+++ b/src/engine/internal/engine.h
@@ -206,7 +206,6 @@ class Engine : public IEngine
         std::vector<std::shared_ptr<Thread>> stepThreads();
         void stepThread(std::shared_ptr<Thread> thread);
         void eventLoop(bool untilProjectStops = false);
-        void finalize();
         void deleteClones();
         void removeExecutableClones();
         void addVarOrListMonitor(std::shared_ptr<Monitor> monitor, Target *target);

From ca5524344786afe5d703575ae02f202f375d1dff Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Sun, 2 Mar 2025 12:06:36 +0100
Subject: [PATCH 11/15] Engine: Ignore frame activity after project stops in
 event loop

---
 src/engine/internal/engine.cpp | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/engine/internal/engine.cpp b/src/engine/internal/engine.cpp
index 5f0da4bd7..59cc29e9b 100644
--- a/src/engine/internal/engine.cpp
+++ b/src/engine/internal/engine.cpp
@@ -779,6 +779,7 @@ void Engine::eventLoop(bool untilProjectStops)
     m_eventLoopMutex.lock();
     m_threads.clear();
     m_running = false;
+    m_frameActivity = false;
     m_redrawRequested = false;
     m_eventLoopMutex.unlock();
 }

From 3daacaa12e1b2906f5a7b48bf89df310167fcfd7 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Sun, 2 Mar 2025 12:07:00 +0100
Subject: [PATCH 12/15] Initialize variables in stop all bypass test

---
 test/stop_all_bypass.sb3 | Bin 1608 -> 1620 bytes
 1 file changed, 0 insertions(+), 0 deletions(-)

diff --git a/test/stop_all_bypass.sb3 b/test/stop_all_bypass.sb3
index b2c826897e2f2c5466081224c97e4b9c621388e8..6d42e0d3683fa2821b84f38fc2273c0972f2b5e2 100644
GIT binary patch

From f03eb79b0c6ff20664d340cf04c05f24203d69ac Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Sun, 2 Mar 2025 12:07:31 +0100
Subject: [PATCH 13/15] Engine: Call onInit() for default extensions

---
 src/engine/internal/engine.cpp | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/src/engine/internal/engine.cpp b/src/engine/internal/engine.cpp
index 59cc29e9b..4fca36c1a 100644
--- a/src/engine/internal/engine.cpp
+++ b/src/engine/internal/engine.cpp
@@ -1486,8 +1486,10 @@ void Engine::setExtensions(const std::vector<std::string> &newExtensions)
     // Register blocks of default extensions
     const auto &defaultExtensions = Blocks::extensions();
 
-    for (auto ext : defaultExtensions)
+    for (auto ext : defaultExtensions) {
         ext->registerBlocks(this);
+        ext->onInit(this);
+    }
 
     // Register blocks of custom extensions
     for (auto ext : m_extensions) {

From 4484d245aa23d149ef8403534387a1886d6eef3f Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Wed, 5 Mar 2025 11:01:53 +0100
Subject: [PATCH 14/15] Engine: Fix use after free when using removed clone
 threads

All threads are copied to m_threadsToStop, including clone threads that are removed later which can lead to use after free when the threadAboutToStop signal is emitted for these threads.
---
 src/engine/internal/engine.cpp | 15 +++++++++------
 1 file changed, 9 insertions(+), 6 deletions(-)

diff --git a/src/engine/internal/engine.cpp b/src/engine/internal/engine.cpp
index 4fca36c1a..35a5d54bc 100644
--- a/src/engine/internal/engine.cpp
+++ b/src/engine/internal/engine.cpp
@@ -362,17 +362,13 @@ void Engine::stop()
     // https://github.com/scratchfoundation/scratch-vm/blob/f1aa92fad79af17d9dd1c41eeeadca099339a9f1/src/engine/runtime.js#L2057-L2081
     if (m_activeThread) {
         stopThread(m_activeThread.get());
-        // NOTE: The project should continue running even after "stop all" is called and the remaining threads should be stepped once.
-        // The remaining threads can even start new threads which will ignore the "stop all" call and will "restart" the project.
-        // This is probably a bug in the Scratch VM, but let's keep it here to keep it compatible.
-        m_threadsToStop = m_threads;
 
         // Remove threads owned by clones because clones are going to be deleted (#547)
         m_threads.erase(
             std::remove_if(
                 m_threads.begin(),
                 m_threads.end(),
-                [](std::shared_ptr<Thread> thread) {
+                [this](std::shared_ptr<Thread> thread) {
                     assert(thread);
                     Target *target = thread->target();
                     assert(target);
@@ -380,13 +376,20 @@ void Engine::stop()
                     if (!target->isStage()) {
                         Sprite *sprite = static_cast<Sprite *>(target);
 
-                        if (sprite->isClone())
+                        if (sprite->isClone()) {
+                            m_threadAboutToStop(thread.get());
                             return true;
+                        }
                     }
 
                     return false;
                 }),
             m_threads.end());
+
+        // NOTE: The project should continue running even after "stop all" is called and the remaining threads should be stepped once.
+        // The remaining threads can even start new threads which will ignore the "stop all" call and will "restart" the project.
+        // This is probably a bug in the Scratch VM, but let's keep it here to keep it compatible.
+        m_threadsToStop = m_threads;
     } 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

From 064d1ca05d20e80a1b0cb4b93e6b72168efd51f4 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Tue, 25 Mar 2025 22:37:42 +0100
Subject: [PATCH 15/15] fix #637: Ignore invisible text bubbles when changing
 layer

Resolves: #637
---
 src/engine/internal/engine.cpp | 26 ++++++++++++++++-------
 test/engine/engine_test.cpp    | 38 ++++++++++++++++++++++++++++++++++
 2 files changed, 56 insertions(+), 8 deletions(-)

diff --git a/src/engine/internal/engine.cpp b/src/engine/internal/engine.cpp
index 35a5d54bc..e5d8f0cc0 100644
--- a/src/engine/internal/engine.cpp
+++ b/src/engine/internal/engine.cpp
@@ -1324,16 +1324,26 @@ void Engine::moveDrawableForwardLayers(Drawable *drawable, int layers)
     if (it == m_sortedDrawables.end())
         return;
 
-    auto target = it + layers;
+    auto target = it;
+    int layersAbs = std::abs(layers);
 
-    if (target <= m_sortedDrawables.begin()) {
-        moveDrawableToBack(drawable);
-        return;
-    }
+    for (int i = 0; i < layersAbs; i++) {
+        if (target <= m_sortedDrawables.begin()) {
+            moveDrawableToBack(drawable);
+            return;
+        }
 
-    if (target >= m_sortedDrawables.end()) {
-        moveDrawableToFront(drawable);
-        return;
+        if (target >= m_sortedDrawables.end()) {
+            moveDrawableToFront(drawable);
+            return;
+        }
+
+        Drawable *currentDrawable;
+
+        do {
+            currentDrawable = *target;
+            target += layers / layersAbs;
+        } while (currentDrawable->isTextBubble() && static_cast<TextBubble *>(currentDrawable)->text().empty());
     }
 
     if (layers > 0)
diff --git a/test/engine/engine_test.cpp b/test/engine/engine_test.cpp
index cb0c17b68..944f5e05c 100644
--- a/test/engine/engine_test.cpp
+++ b/test/engine/engine_test.cpp
@@ -1362,6 +1362,11 @@ TEST(EngineTest, MoveDrawableForwardLayers)
     std::vector<Sprite *> sprites;
     createTargets(&engine, sprites);
 
+    const auto &targets = engine.targets();
+
+    for (auto target : targets)
+        target->bubble()->setText("test");
+
     engine.moveDrawableForwardLayers(sprites[4], 2);
     ASSERT_EQ(sprites[0]->layerOrder(), 1);
     ASSERT_EQ(sprites[1]->layerOrder(), 5);
@@ -1421,6 +1426,20 @@ TEST(EngineTest, MoveDrawableForwardLayers)
     ASSERT_EQ(sprites[2]->bubble()->layerOrder(), 8);
     ASSERT_EQ(sprites[3]->bubble()->layerOrder(), 9);
     ASSERT_EQ(sprites[4]->bubble()->layerOrder(), 11);
+
+    sprites[1]->bubble()->setText("");
+    sprites[3]->bubble()->setText("");
+    engine.moveDrawableForwardLayers(sprites[2], 8);
+    ASSERT_EQ(sprites[0]->layerOrder(), 1);
+    ASSERT_EQ(sprites[1]->layerOrder(), 4);
+    ASSERT_EQ(sprites[2]->layerOrder(), 11);
+    ASSERT_EQ(sprites[3]->layerOrder(), 3);
+    ASSERT_EQ(sprites[4]->layerOrder(), 2);
+    ASSERT_EQ(sprites[0]->bubble()->layerOrder(), 9);
+    ASSERT_EQ(sprites[1]->bubble()->layerOrder(), 6);
+    ASSERT_EQ(sprites[2]->bubble()->layerOrder(), 7);
+    ASSERT_EQ(sprites[3]->bubble()->layerOrder(), 8);
+    ASSERT_EQ(sprites[4]->bubble()->layerOrder(), 10);
 }
 
 TEST(EngineTest, MoveDrawableBackwardLayers)
@@ -1429,6 +1448,11 @@ TEST(EngineTest, MoveDrawableBackwardLayers)
     std::vector<Sprite *> sprites;
     createTargets(&engine, sprites);
 
+    const auto &targets = engine.targets();
+
+    for (auto target : targets)
+        target->bubble()->setText("test");
+
     engine.moveDrawableBackwardLayers(sprites[4], -2);
     ASSERT_EQ(sprites[0]->layerOrder(), 1);
     ASSERT_EQ(sprites[1]->layerOrder(), 5);
@@ -1488,6 +1512,20 @@ TEST(EngineTest, MoveDrawableBackwardLayers)
     ASSERT_EQ(sprites[2]->bubble()->layerOrder(), 8);
     ASSERT_EQ(sprites[3]->bubble()->layerOrder(), 9);
     ASSERT_EQ(sprites[4]->bubble()->layerOrder(), 11);
+
+    sprites[1]->bubble()->setText("");
+    sprites[3]->bubble()->setText("");
+    engine.moveDrawableBackwardLayers(sprites[2], -8);
+    ASSERT_EQ(sprites[0]->layerOrder(), 1);
+    ASSERT_EQ(sprites[1]->layerOrder(), 4);
+    ASSERT_EQ(sprites[2]->layerOrder(), 11);
+    ASSERT_EQ(sprites[3]->layerOrder(), 3);
+    ASSERT_EQ(sprites[4]->layerOrder(), 2);
+    ASSERT_EQ(sprites[0]->bubble()->layerOrder(), 9);
+    ASSERT_EQ(sprites[1]->bubble()->layerOrder(), 6);
+    ASSERT_EQ(sprites[2]->bubble()->layerOrder(), 7);
+    ASSERT_EQ(sprites[3]->bubble()->layerOrder(), 8);
+    ASSERT_EQ(sprites[4]->bubble()->layerOrder(), 10);
 }
 
 TEST(EngineTest, MoveDrawableBehindOther)
