#include "MetaCoreScripting/MetaCoreScripting.h" #include #include #include #include #include #include #include #include #include namespace { void Expect(bool condition, const char* message) { if (!condition) throw std::runtime_error(message); } struct Counters { int Start = 0; int Fixed = 0; int Update = 0; int Late = 0; int Stop = 0; }; class CountingBehaviour final : public MetaCore::MetaCoreIScriptBehaviour { public: explicit CountingBehaviour(std::shared_ptr counters) : Counters_(std::move(counters)) {} void OnPlayStart(MetaCore::MetaCoreScriptExecutionContext&) override { ++Counters_->Start; } void FixedUpdate(MetaCore::MetaCoreScriptExecutionContext&, double) override { ++Counters_->Fixed; } void Update(MetaCore::MetaCoreScriptExecutionContext&, double) override { ++Counters_->Update; } void LateUpdate(MetaCore::MetaCoreScriptExecutionContext&, double) override { ++Counters_->Late; } void OnPlayStop(MetaCore::MetaCoreScriptExecutionContext&) override { ++Counters_->Stop; } private: std::shared_ptr Counters_; }; class FaultBehaviour final : public MetaCore::MetaCoreIScriptBehaviour { public: void Update(MetaCore::MetaCoreScriptExecutionContext&, double) override { throw std::runtime_error("expected fault"); } }; void TestRegistryFieldsAndReplacement() { MetaCore::MetaCoreScriptRegistry registry; MetaCore::MetaCoreScriptDefinition definition; definition.TypeId = "Tests.Fields"; definition.ModuleId = "Tests"; MetaCore::MetaCoreScriptFieldDefinition speed{"OldSpeed", "Speed", MetaCore::MetaCoreScriptFieldType::Float, false, 2.0F}; speed.FieldId = "speed"; MetaCore::MetaCoreScriptFieldDefinition target{"Target", "Target", MetaCore::MetaCoreScriptFieldType::GameObjectRef}; target.FieldId = "target"; definition.Fields = {speed, target}; definition.Factory = [] { return std::make_unique(std::make_shared()); }; std::string error; Expect(registry.RegisterScript(definition, &error), "valid definition should register"); const auto migrated = nlohmann::json::parse(registry.EnsureFieldsJsonDefaults("Tests.Fields", R"({"OldSpeed":4.5,"unknown":7})")); Expect(migrated.value("speed", 0.0) == 4.5, "legacy field name should migrate to stable id"); Expect(!migrated.contains("OldSpeed") && migrated.value("unknown", 0) == 7, "migration should preserve unknown fields and remove migrated key"); Expect(migrated.contains("target"), "new reference fields should receive defaults"); auto duplicate = definition; duplicate.TypeId = "Tests.Other"; duplicate.Fields.push_back(speed); Expect(!registry.ReplaceModule("Tests", {definition, duplicate}, &error), "duplicate field ids should reject the complete candidate"); Expect(registry.FindScript("Tests.Fields") != nullptr, "failed replacement should preserve old registry state"); } void TestRuntimeLifecycleFaultsHandlesEventsAndTimers() { MetaCore::MetaCoreScene scene; auto object = scene.CreateGameObject("Scripted"); auto& scripts = object.AddComponent(); scripts.Instances.push_back({11, "Tests.Count", true, "{}"}); scripts.Instances.push_back({12, "Tests.Fault", true, "{}"}); const auto objectId = object.GetId(); auto counters = std::make_shared(); MetaCore::MetaCoreScriptRegistry registry; MetaCore::MetaCoreScriptDefinition count; count.TypeId = "Tests.Count"; count.Factory = [counters] { return std::make_unique(counters); }; MetaCore::MetaCoreScriptDefinition fault; fault.TypeId = "Tests.Fault"; fault.Factory = [] { return std::make_unique(); }; Expect(registry.RegisterScript(std::move(count)), "count script registration"); Expect(registry.RegisterScript(std::move(fault)), "fault script registration"); std::vector logs; MetaCore::MetaCoreScriptRuntime runtime(scene, registry, [&](std::uint32_t, std::string_view, std::string_view message) { logs.emplace_back(message); }); runtime.Start(); const MetaCoreScriptObjectHandleV1 handle{runtime.GetWorldGeneration(), objectId}; Expect(runtime.IsHandleValid(handle), "current world/object handle should be valid"); runtime.FixedUpdate(1.0 / 60.0); runtime.Update(0.1); runtime.LateUpdate(0.1); Expect(counters->Start == 1 && counters->Fixed == 1 && counters->Update == 1 && counters->Late == 1, "runtime should dispatch lifecycle in all phases"); Expect(runtime.GetFaultedInstanceCount() == 1 && !logs.empty(), "one throwing behaviour should be isolated and diagnosed"); std::vector eventOrder; runtime.Subscribe(objectId, 11, "first", [&](std::string_view, const auto&) { eventOrder.push_back("first"); runtime.Publish("second"); }); runtime.Subscribe(objectId, 11, "second", [&](std::string_view, const auto&) { eventOrder.push_back("second"); }); runtime.Publish("first"); runtime.Update(0.1); Expect(eventOrder == std::vector{"first"}, "events published during dispatch should wait for the next batch"); runtime.Update(0.1); Expect(eventOrder == std::vector({"first", "second"}), "next event batch should preserve publication order"); int timerCount = 0; runtime.ScheduleTimer(objectId, 11, 0.05, 0.0, false, [&] { ++timerCount; }); runtime.Update(0.1); Expect(timerCount == 1, "one-shot timer should fire once"); runtime.ScheduleTimer(objectId, 11, 0.0, 0.01, false, [&] { ++timerCount; }); (void)scene.DeleteGameObjects({objectId}); runtime.Update(0.1); Expect(timerCount == 1, "deleting the owner should cancel timers before callback"); runtime.Stop(); Expect(counters->Stop == 0, "deleted owner should not receive OnPlayStop"); Expect(!runtime.IsHandleValid(handle), "handles should invalidate after Stop/world generation change"); } void TestGeneratedProjectBuildAndLoad() { const auto root = std::filesystem::temp_directory_path() / "MetaCoreScriptingTestsProject"; std::error_code errorCode; std::filesystem::remove_all(root, errorCode); std::filesystem::create_directories(root, errorCode); std::string error; Expect(MetaCore::MetaCoreGenerateScriptProject(root, "ScriptingTests", METACORE_SOURCE_ROOT, &error), error.c_str()); Expect(MetaCore::MetaCoreGenerateScriptProject(root, "ScriptingTests", METACORE_SOURCE_ROOT, &error), "script project generation should be idempotent"); Expect(MetaCore::MetaCoreCreateScriptBehaviour(root, "ScriptingTests", "SecondBehaviour", &error), error.c_str()); Expect(std::filesystem::is_regular_file(root / "Scripts" / "SDK" / "MetaCoreScripting" / "MetaCoreScriptAbi.h"), "generated project should contain a self-contained ABI SDK"); const auto build = MetaCore::MetaCoreBuildScriptProject(root, "Debug", METACORE_SOURCE_ROOT); Expect(build.Success, build.Output.c_str()); Expect(std::filesystem::is_regular_file(root / "compile_commands.json"), "script build should publish compile_commands.json"); MetaCore::MetaCoreScriptModuleLoader loader; auto module = loader.LoadCandidate(build.ModulePath, &error); Expect(module.has_value(), error.c_str()); Expect(module->ModuleId == "ScriptingTests" && module->Definitions.size() == 2, "generated module should expose all generated behaviour descriptors"); MetaCore::MetaCoreScriptRegistry registry; Expect(registry.ReplaceModule(module->ModuleId, module->Definitions, &error), error.c_str()); Expect(registry.FindScript("ScriptingTests.Mover") != nullptr, "loaded project behaviour should be attachable"); Expect(registry.FindScript("ScriptingTests.SecondBehaviour") != nullptr, "newly created behaviour should be compiled and attachable"); { std::ofstream corruptManifest(build.ManifestPath, std::ios::trunc); corruptManifest << R"({"format_version":1,"abi_major":1,"abi_minor":0,"module_id":"ScriptingTests","module_file":")" << build.ModulePath.filename().string() << R"(","sha256":"bad"})"; } auto rejected = loader.LoadCandidate(build.ModulePath, &error); Expect(!rejected.has_value(), "module with a mismatched manifest hash should be rejected before activation"); Expect(registry.FindScript("ScriptingTests.Mover") != nullptr, "failed candidate validation should leave the active registry intact"); std::filesystem::remove_all(root, errorCode); } } // namespace int main() { try { TestRegistryFieldsAndReplacement(); TestRuntimeLifecycleFaultsHandlesEventsAndTimers(); TestGeneratedProjectBuildAndLoad(); std::cout << "MetaCoreScriptingTests passed\n"; return 0; } catch (const std::exception& error) { std::cerr << "MetaCoreScriptingTests failed: " << error.what() << '\n'; return 1; } }