diff --git a/include/scratchcpp/target.h b/include/scratchcpp/target.h index fe9e7e23..0cb991f5 100644 --- a/include/scratchcpp/target.h +++ b/include/scratchcpp/target.h @@ -24,6 +24,7 @@ class LIBSCRATCHCPP_EXPORT Target public: Target(); Target(const Target &) = delete; + virtual ~Target() { } /*! Returns true if this Target is the stage. */ virtual bool isStage() const { return false; } diff --git a/src/blocks/controlblocks.cpp b/src/blocks/controlblocks.cpp index 27008793..9da72683 100644 --- a/src/blocks/controlblocks.cpp +++ b/src/blocks/controlblocks.cpp @@ -2,7 +2,10 @@ #include #include +#include +#include #include +#include #include #include "controlblocks.h" @@ -27,6 +30,9 @@ void ControlBlocks::registerBlocks(IEngine *engine) engine->addCompileFunction(this, "control_stop", &compileStop); engine->addCompileFunction(this, "control_wait", &compileWait); engine->addCompileFunction(this, "control_wait_until", &compileWaitUntil); + engine->addCompileFunction(this, "control_start_as_clone", &compileStartAsClone); + engine->addCompileFunction(this, "control_create_clone_of", &compileCreateClone); + engine->addCompileFunction(this, "control_delete_this_clone", &compileDeleteThisClone); // Inputs engine->addInput(this, "SUBSTACK", SUBSTACK); @@ -35,6 +41,7 @@ void ControlBlocks::registerBlocks(IEngine *engine) engine->addInput(this, "CONDITION", CONDITION); engine->addInput(this, "DURATION", DURATION); engine->addInput(this, "VALUE", VALUE); + engine->addInput(this, "CLONE_OPTION", CLONE_OPTION); // Fields engine->addField(this, "STOP_OPTION", STOP_OPTION); @@ -155,6 +162,37 @@ void ControlBlocks::compileWaitUntil(Compiler *compiler) compiler->addFunctionCall(&waitUntil); } +void ControlBlocks::compileStartAsClone(Compiler *compiler) +{ + compiler->engine()->addCloneInitScript(compiler->block()); +} + +void ControlBlocks::compileCreateClone(Compiler *compiler) +{ + Input *input = compiler->input(CLONE_OPTION); + + if (input->type() != Input::Type::ObscuredShadow) { + assert(input->pointsToDropdownMenu()); + std::string spriteName = input->selectedMenuItem(); + + if (spriteName == "_myself_") + compiler->addFunctionCall(&createCloneOfMyself); + else { + int index = compiler->engine()->findTarget(spriteName); + compiler->addConstValue(index); + compiler->addFunctionCall(&createCloneByIndex); + } + } else { + compiler->addInput(input); + compiler->addFunctionCall(&createClone); + } +} + +void ControlBlocks::compileDeleteThisClone(Compiler *compiler) +{ + compiler->addFunctionCall(&deleteThisClone); +} + unsigned int ControlBlocks::stopAll(VirtualMachine *vm) { vm->engine()->stop(); @@ -196,3 +234,54 @@ unsigned int ControlBlocks::waitUntil(VirtualMachine *vm) } return 1; } + +unsigned int ControlBlocks::createClone(VirtualMachine *vm) +{ + std::string spriteName = vm->getInput(0, 1)->toString(); + Target *target; + + if (spriteName == "_myself_") + target = vm->target(); + else + target = vm->engine()->targetAt(vm->engine()->findTarget(spriteName)); + + Sprite *sprite = dynamic_cast(target); + + if (sprite) + sprite->clone(); + + return 1; +} + +unsigned int ControlBlocks::createCloneByIndex(VirtualMachine *vm) +{ + Target *target = vm->engine()->targetAt(vm->getInput(0, 1)->toInt()); + Sprite *sprite = dynamic_cast(target); + + if (sprite) + sprite->clone(); + + return 1; +} + +unsigned int ControlBlocks::createCloneOfMyself(VirtualMachine *vm) +{ + Sprite *sprite = dynamic_cast(vm->target()); + + if (sprite) + sprite->clone(); + + return 0; +} + +unsigned int ControlBlocks::deleteThisClone(VirtualMachine *vm) +{ + Target *target = vm->target(); + + if (target) { + vm->engine()->stopTarget(target, nullptr); + target->~Target(); + } + + return 0; +} diff --git a/src/blocks/controlblocks.h b/src/blocks/controlblocks.h index ec35efce..92e63aaf 100644 --- a/src/blocks/controlblocks.h +++ b/src/blocks/controlblocks.h @@ -23,7 +23,8 @@ class ControlBlocks : public IBlockSection TIMES, CONDITION, DURATION, - VALUE + VALUE, + CLONE_OPTION }; enum Fields @@ -53,12 +54,19 @@ class ControlBlocks : public IBlockSection static void compileStop(Compiler *compiler); static void compileWait(Compiler *compiler); static void compileWaitUntil(Compiler *compiler); + static void compileStartAsClone(Compiler *compiler); + static void compileCreateClone(Compiler *compiler); + static void compileDeleteThisClone(Compiler *compiler); static unsigned int stopAll(VirtualMachine *vm); static unsigned int stopOtherScriptsInSprite(VirtualMachine *vm); static unsigned int startWait(VirtualMachine *vm); static unsigned int wait(VirtualMachine *vm); static unsigned int waitUntil(VirtualMachine *vm); + static unsigned int createClone(VirtualMachine *vm); + static unsigned int createCloneByIndex(VirtualMachine *vm); + static unsigned int createCloneOfMyself(VirtualMachine *vm); + static unsigned int deleteThisClone(VirtualMachine *vm); static inline std::unordered_map> m_timeMap; }; diff --git a/src/engine/internal/engine.cpp b/src/engine/internal/engine.cpp index 3572e4fa..b38bf80c 100644 --- a/src/engine/internal/engine.cpp +++ b/src/engine/internal/engine.cpp @@ -143,7 +143,8 @@ void Engine::frame() } } - m_scriptsToRemove.push_back(script.get()); + if (std::find(m_scriptsToRemove.begin(), m_scriptsToRemove.end(), script.get()) == m_scriptsToRemove.end()) + m_scriptsToRemove.push_back(script.get()); } } while (!script->atEnd() && !m_breakFrame); } @@ -262,7 +263,8 @@ void Engine::broadcast(unsigned int index, VirtualMachine *sourceScript, bool wa void Engine::stopScript(VirtualMachine *vm) { assert(vm); - m_scriptsToRemove.push_back(vm); + if (std::find(m_scriptsToRemove.begin(), m_scriptsToRemove.end(), vm) == m_scriptsToRemove.end()) + m_scriptsToRemove.push_back(vm); } void Engine::stopTarget(Target *target, VirtualMachine *exceptScript) @@ -298,7 +300,7 @@ void Engine::initClone(Sprite *clone) #ifndef NDEBUG // Since we're initializing the clone, it shouldn't have any running scripts for (const auto script : m_runningScripts) - assert(script->target() != clone); + assert((script->target() != clone) || (std::find(m_scriptsToRemove.begin(), m_scriptsToRemove.end(), script.get()) != m_scriptsToRemove.end())); #endif for (auto script : scripts) { diff --git a/test/blocks/control_blocks_test.cpp b/test/blocks/control_blocks_test.cpp index fa87f014..dab49c20 100644 --- a/test/blocks/control_blocks_test.cpp +++ b/test/blocks/control_blocks_test.cpp @@ -3,15 +3,19 @@ #include #include #include +#include #include #include "../common.h" #include "blocks/controlblocks.h" +#include "blocks/operatorblocks.h" #include "engine/internal/engine.h" using namespace libscratchcpp; using ::testing::Return; +using ::testing::_; +using ::testing::SaveArg; class ControlBlocksTest : public testing::Test { @@ -25,6 +29,23 @@ class ControlBlocksTest : public testing::Test // For any control block std::shared_ptr createControlBlock(const std::string &id, const std::string &opcode) const { return std::make_shared(id, opcode); } + // For control_create_clone_of + std::shared_ptr createCloneBlock(const std::string &id, const std::string &spriteName, std::shared_ptr valueBlock = nullptr) + { + auto block = createControlBlock(id, "control_create_clone_of"); + + if (valueBlock) + addObscuredInput(block, "CLONE_OPTION", ControlBlocks::CLONE_OPTION, valueBlock); + else { + auto input = addNullInput(block, "CLONE_OPTION", ControlBlocks::CLONE_OPTION); + auto menu = createControlBlock(id + "_menu", "control_create_clone_of_menu"); + input->setValueBlock(menu); + addDropdownField(menu, "CLONE_OPTION", static_cast(-1), spriteName, static_cast(-1)); + } + + return block; + } + void addSubstackInput(std::shared_ptr block, const std::string &name, ControlBlocks::Inputs id, std::shared_ptr valueBlock) const { auto input = std::make_shared(name, Input::Type::NoShadow); @@ -52,12 +73,14 @@ class ControlBlocksTest : public testing::Test block->updateInputMap(); } - void addNullInput(std::shared_ptr block, const std::string &name, ControlBlocks::Inputs id) const + std::shared_ptr addNullInput(std::shared_ptr block, const std::string &name, ControlBlocks::Inputs id) const { auto input = std::make_shared(name, Input::Type::Shadow); input->setInputId(id); block->addInput(input); block->updateInputMap(); + + return input; } void addDropdownField(std::shared_ptr block, const std::string &name, ControlBlocks::Fields id, const std::string &value, ControlBlocks::FieldValues valueId) const @@ -130,6 +153,9 @@ TEST_F(ControlBlocksTest, RegisterBlocks) EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "control_stop", &ControlBlocks::compileStop)).Times(1); EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "control_wait", &ControlBlocks::compileWait)).Times(1); EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "control_wait_until", &ControlBlocks::compileWaitUntil)).Times(1); + EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "control_start_as_clone", &ControlBlocks::compileStartAsClone)).Times(1); + EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "control_create_clone_of", &ControlBlocks::compileCreateClone)).Times(1); + EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "control_delete_this_clone", &ControlBlocks::compileDeleteThisClone)).Times(1); // Inputs EXPECT_CALL(m_engineMock, addInput(m_section.get(), "SUBSTACK", ControlBlocks::SUBSTACK)); @@ -138,6 +164,7 @@ TEST_F(ControlBlocksTest, RegisterBlocks) EXPECT_CALL(m_engineMock, addInput(m_section.get(), "CONDITION", ControlBlocks::CONDITION)); EXPECT_CALL(m_engineMock, addInput(m_section.get(), "DURATION", ControlBlocks::DURATION)); EXPECT_CALL(m_engineMock, addInput(m_section.get(), "VALUE", ControlBlocks::VALUE)); + EXPECT_CALL(m_engineMock, addInput(m_section.get(), "CLONE_OPTION", ControlBlocks::CLONE_OPTION)); // Fields EXPECT_CALL(m_engineMock, addField(m_section.get(), "STOP_OPTION", ControlBlocks::STOP_OPTION)); @@ -810,3 +837,151 @@ TEST_F(ControlBlocksTest, WaitUntilImpl) ASSERT_EQ(vm.registerCount(), 0); ASSERT_TRUE(vm.atEnd()); } + +TEST_F(ControlBlocksTest, CreateCloneOf) +{ + Compiler compiler(&m_engineMock); + + // create clone of [Sprite1] + auto block1 = createCloneBlock("a", "Sprite1"); + + // create clone of [myself] + auto block2 = createCloneBlock("b", "_myself_"); + + // create clone of (join "" "") + auto joinBlock = std::make_shared("d", "operator_join"); + joinBlock->setCompileFunction(&OperatorBlocks::compileJoin); + auto block3 = createCloneBlock("c", "", joinBlock); + + EXPECT_CALL(m_engineMock, findTarget("Sprite1")).WillOnce(Return(4)); + EXPECT_CALL(m_engineMock, functionIndex(&ControlBlocks::createCloneByIndex)).WillOnce(Return(0)); + EXPECT_CALL(m_engineMock, functionIndex(&ControlBlocks::createCloneOfMyself)).WillOnce(Return(1)); + EXPECT_CALL(m_engineMock, functionIndex(&ControlBlocks::createClone)).WillOnce(Return(2)); + + compiler.init(); + compiler.setBlock(block1); + ControlBlocks::compileCreateClone(&compiler); + compiler.setBlock(block2); + ControlBlocks::compileCreateClone(&compiler); + compiler.setBlock(block3); + ControlBlocks::compileCreateClone(&compiler); + compiler.end(); + + ASSERT_EQ( + compiler.bytecode(), + std::vector({ vm::OP_START, vm::OP_CONST, 0, vm::OP_EXEC, 0, vm::OP_EXEC, 1, vm::OP_NULL, vm::OP_NULL, vm::OP_STR_CONCAT, vm::OP_EXEC, 2, vm::OP_HALT })); + ASSERT_EQ(compiler.constValues().size(), 1); + ASSERT_EQ(compiler.constValues()[0].toDouble(), 4); + ASSERT_TRUE(compiler.variables().empty()); + ASSERT_TRUE(compiler.lists().empty()); +} + +TEST_F(ControlBlocksTest, CreateCloneOfImpl) +{ + 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_EXEC, 1, vm::OP_HALT }; + static unsigned int bytecode3[] = { vm::OP_START, vm::OP_CONST, 1, vm::OP_EXEC, 2, vm::OP_HALT }; + static unsigned int bytecode4[] = { vm::OP_START, vm::OP_CONST, 2, vm::OP_EXEC, 2, vm::OP_HALT }; + static BlockFunc functions[] = { &ControlBlocks::createCloneByIndex, &ControlBlocks::createCloneOfMyself, &ControlBlocks::createClone }; + static Value constValues[] = { 4, "Sprite1", "_myself_" }; + + Sprite sprite; + sprite.setEngine(&m_engineMock); + + VirtualMachine vm(&sprite, &m_engineMock, nullptr); + vm.setFunctions(functions); + vm.setConstValues(constValues); + + EXPECT_CALL(m_engineMock, targetAt(4)).WillOnce(Return(&sprite)); + EXPECT_CALL(m_engineMock, initClone).Times(1); + + vm.setBytecode(bytecode1); + vm.run(); + + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(sprite.allChildren().size(), 1); + + EXPECT_CALL(m_engineMock, initClone).Times(1); + + vm.setBytecode(bytecode2); + vm.run(); + + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(sprite.allChildren().size(), 2); + ASSERT_EQ(sprite.children(), sprite.allChildren()); + + EXPECT_CALL(m_engineMock, findTarget).WillOnce(Return(4)); + EXPECT_CALL(m_engineMock, targetAt(4)).WillOnce(Return(&sprite)); + EXPECT_CALL(m_engineMock, initClone).Times(1); + + vm.setBytecode(bytecode3); + vm.run(); + + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(sprite.allChildren().size(), 3); + ASSERT_EQ(sprite.children(), sprite.allChildren()); + + EXPECT_CALL(m_engineMock, initClone).Times(1); + + vm.setBytecode(bytecode4); + vm.run(); + + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(sprite.allChildren().size(), 4); + ASSERT_EQ(sprite.children(), sprite.allChildren()); +} + +TEST_F(ControlBlocksTest, StartAsClone) +{ + Compiler compiler(&m_engineMock); + + auto block = createControlBlock("a", "control_start_as_clone"); + compiler.setBlock(block); + + EXPECT_CALL(m_engineMock, addCloneInitScript(block)).Times(1); + ControlBlocks::compileStartAsClone(&compiler); +} + +TEST_F(ControlBlocksTest, DeleteThisClone) +{ + Compiler compiler(&m_engineMock); + + auto block = createControlBlock("a", "control_delete_this_clone"); + + EXPECT_CALL(m_engineMock, functionIndex(&ControlBlocks::deleteThisClone)).WillOnce(Return(0)); + + compiler.init(); + compiler.setBlock(block); + ControlBlocks::compileDeleteThisClone(&compiler); + compiler.end(); + + ASSERT_EQ(compiler.bytecode(), std::vector({ vm::OP_START, vm::OP_EXEC, 0, vm::OP_HALT })); + ASSERT_TRUE(compiler.constValues().empty()); + ASSERT_TRUE(compiler.variables().empty()); + ASSERT_TRUE(compiler.lists().empty()); +} + +TEST_F(ControlBlocksTest, DeleteThisCloneImpl) +{ + static unsigned int bytecode[] = { vm::OP_START, vm::OP_EXEC, 0, vm::OP_HALT }; + static BlockFunc functions[] = { &ControlBlocks::deleteThisClone }; + + Sprite sprite; + sprite.setEngine(&m_engineMock); + + Sprite *clone; + EXPECT_CALL(m_engineMock, initClone(_)).WillOnce(SaveArg<0>(&clone)); + sprite.clone(); + ASSERT_TRUE(clone); + + VirtualMachine vm(clone, &m_engineMock, nullptr); + vm.setFunctions(functions); + + EXPECT_CALL(m_engineMock, stopTarget(clone, nullptr)).Times(1); + + vm.setBytecode(bytecode); + vm.run(); + + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_TRUE(sprite.children().empty()); +} diff --git a/test/clones.sb3 b/test/clones.sb3 new file mode 100644 index 00000000..2325078d Binary files /dev/null and b/test/clones.sb3 differ diff --git a/test/engine/engine_test.cpp b/test/engine/engine_test.cpp index f15364c9..cd45c1d2 100644 --- a/test/engine/engine_test.cpp +++ b/test/engine/engine_test.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include @@ -331,3 +332,48 @@ TEST(EngineTest, ListOwner) ASSERT_EQ(engine.listOwner(list3.get()), t3.get()); ASSERT_EQ(engine.listOwner(list4.get()), t1.get()); } + +TEST(EngineTest, Clones) +{ + Project p("clones.sb3"); + ASSERT_TRUE(p.load()); + p.run(); + + auto engine = p.engine(); + + Target *stage = engine->targetAt(engine->findTarget("Stage")); + ASSERT_TRUE(stage); + + ASSERT_VAR(stage, "clone1"); + ASSERT_EQ(GET_VAR(stage, "clone1")->value().toInt(), 1); + ASSERT_VAR(stage, "clone2"); + ASSERT_EQ(GET_VAR(stage, "clone2")->value().toInt(), 1); + ASSERT_VAR(stage, "clone3"); + ASSERT_EQ(GET_VAR(stage, "clone3")->value().toInt(), 1); + ASSERT_VAR(stage, "clone4"); + ASSERT_EQ(GET_VAR(stage, "clone4")->value().toInt(), 1); + ASSERT_VAR(stage, "clone5"); + ASSERT_EQ(GET_VAR(stage, "clone5")->value().toInt(), 110); + ASSERT_VAR(stage, "delete_passed"); + ASSERT_TRUE(GET_VAR(stage, "delete_passed")->value().toBool()); + + ASSERT_LIST(stage, "log1"); + auto list = GET_LIST(stage, "log1"); + + for (int i = 0; i < list->size(); i++) { + if (i < 10) + ASSERT_EQ((*list)[i].toInt(), 1); + else + ASSERT_EQ((*list)[i].toInt(), 2); + } + + ASSERT_LIST(stage, "log2"); + list = GET_LIST(stage, "log2"); + + for (int i = 0; i < list->size(); i++) { + if (i < 10) + ASSERT_EQ((*list)[i].toInt(), 1); + else + ASSERT_EQ((*list)[i].toString(), "1 2"); // TODO: Change this to "12" after #188 is fixed + } +}