From 204ece778fdc25c2d046b30c52138bd469ccf934 Mon Sep 17 00:00:00 2001 From: Rowland <975945824@qq.com> Date: Thu, 16 Jul 2026 09:01:25 +0800 Subject: [PATCH] =?UTF-8?q?=E8=84=9A=E6=9C=AC=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Apps/MetaCorePlayer/main.cpp | 46 ++ CMakeLists.txt | 26 +- .../Private/MetaCoreBuildPipeline.cpp | 76 +- .../MetaCoreDelivery/MetaCoreBuildPipeline.h | 1 + .../MetaCoreBuiltinCoreServicesModule.cpp | 588 +++++++------- .../Private/MetaCoreEditorApp.cpp | 5 + .../Private/MetaCoreEditorServices.cpp | 32 - .../MetaCoreEditor/MetaCoreEditorServices.h | 67 +- .../Private/MetaCoreProject.cpp | 3 +- .../Private/MetaCoreScripting.cpp | 736 ++++++++++++++++++ .../MetaCoreScripting/MetaCoreScriptAbi.h | 130 ++++ .../MetaCoreScripting/MetaCoreScripting.h | 187 +++++ ...etacore-mature-engine-gap-and-work-plan.md | 30 +- tests/MetaCoreDeliveryTests.cpp | 6 + tests/MetaCoreScriptingTests.cpp | 158 ++++ 15 files changed, 1685 insertions(+), 406 deletions(-) create mode 100644 Source/MetaCoreScripting/Private/MetaCoreScripting.cpp create mode 100644 Source/MetaCoreScripting/Public/MetaCoreScripting/MetaCoreScriptAbi.h create mode 100644 Source/MetaCoreScripting/Public/MetaCoreScripting/MetaCoreScripting.h create mode 100644 tests/MetaCoreScriptingTests.cpp diff --git a/Apps/MetaCorePlayer/main.cpp b/Apps/MetaCorePlayer/main.cpp index 31780d4..570eb14 100644 --- a/Apps/MetaCorePlayer/main.cpp +++ b/Apps/MetaCorePlayer/main.cpp @@ -12,6 +12,7 @@ #include "MetaCoreRuntimeUi/MetaCoreRuntimeUiSystem.h" #include "MetaCoreScene/MetaCoreScenePackage.h" #include "MetaCoreScene/MetaCoreScene.h" +#include "MetaCoreScripting/MetaCoreScripting.h" #include #include @@ -379,6 +380,46 @@ int main(int argc, char* argv[]) { return 2; } MetaCore::MetaCoreRuntimeDataDispatcher runtimeDataDispatcher(scene); + MetaCore::MetaCoreScriptRegistry scriptRegistry; + MetaCore::MetaCoreRegisterBuiltinScripts(scriptRegistry); + MetaCore::MetaCoreScriptModuleLoader scriptModuleLoader; + std::vector loadedScriptModules; + std::vector scriptModulePaths; + if (deliveryDocument) { + for (const auto& relative : deliveryDocument->ScriptModules) scriptModulePaths.push_back(packageRoot / relative); + } else if (const auto latest = MetaCore::MetaCoreFindLatestScriptModule(projectRoot); latest.has_value()) { + scriptModulePaths.push_back(*latest); + } + for (const auto& modulePath : scriptModulePaths) { + std::string scriptError; + auto module = scriptModuleLoader.LoadCandidate(modulePath, &scriptError); + if (!module.has_value() || !scriptRegistry.ReplaceModule(module->ModuleId, module->Definitions, &scriptError)) { + errors << "MetaCorePlayer: script module load failed path=" << modulePath << " error=" << scriptError << '\n'; + if (deliveryDocument) return 2; + continue; + } + output << "MetaCorePlayer: loaded script module id=" << module->ModuleId << " build_id=" << module->BuildId << '\n'; + loadedScriptModules.push_back(std::move(*module)); + } + bool missingRequiredScript = false; + for (auto object : scene.GetGameObjects()) { + if (!object.HasComponent()) continue; + for (const auto& instance : object.GetComponent().Instances) { + if (!instance.ScriptTypeId.empty() && scriptRegistry.FindScript(instance.ScriptTypeId) == nullptr) { + errors << "MetaCorePlayer: missing script type=" << instance.ScriptTypeId << " object=" << object.GetId() << '\n'; + missingRequiredScript = true; + } + } + } + if (deliveryDocument && missingRequiredScript) return 2; + MetaCore::MetaCoreScriptRuntime scriptRuntime( + scene, scriptRegistry, + [&output, &errors](std::uint32_t level, std::string_view category, std::string_view message) { + std::ostream& stream = level >= 2U ? errors : output; + stream << "MetaCorePlayer Script[" << category << "]: " << message << '\n'; + } + ); + scriptRuntime.Start(); const auto sourcesPath = (projectRoot / runtimeProjectDocument.DataSourcesPath).lexically_normal(); const auto bindingsPath = (projectRoot / runtimeProjectDocument.BindingsPath).lexically_normal(); @@ -457,6 +498,9 @@ int main(int argc, char* argv[]) { std::uint64_t renderedFrames = 0U; while (!window.ShouldClose()) { window.BeginFrame(); + const double frameDeltaSeconds = window.GetDeltaSeconds(); + scriptRuntime.FixedUpdate(1.0 / 60.0); + scriptRuntime.Update(frameDeltaSeconds); const auto [windowWidth, windowHeight] = window.GetWindowSize(); viewportRenderer.SetViewportRect(MetaCore::MetaCoreViewportRect{ 0.0F, @@ -513,6 +557,7 @@ int main(int argc, char* argv[]) { } } runtimeUi.Update(window.GetDeltaSeconds()); + scriptRuntime.LateUpdate(frameDeltaSeconds); viewportRenderer.RenderSceneToViewport(scene, MetaCoreBuildPlayerSceneView(), true); viewportRenderer.RenderAll(); renderDevice.RenderFrame(); @@ -522,6 +567,7 @@ int main(int argc, char* argv[]) { if (smokeTestFrames > 0U && renderedFrames >= smokeTestFrames) break; } + scriptRuntime.Stop(); runtimeUi.Shutdown(); viewportRenderer.Shutdown(); renderDevice.Shutdown(); diff --git a/CMakeLists.txt b/CMakeLists.txt index 012a102..e1f0fb3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -478,6 +478,21 @@ target_link_libraries(MetaCoreScene target_compile_options(MetaCoreScene PRIVATE ${METACORE_COMMON_WARNINGS}) +add_library(MetaCoreScripting STATIC + Source/MetaCoreScripting/Public/MetaCoreScripting/MetaCoreScriptAbi.h + Source/MetaCoreScripting/Public/MetaCoreScripting/MetaCoreScripting.h + Source/MetaCoreScripting/Private/MetaCoreScripting.cpp +) +target_include_directories(MetaCoreScripting + PUBLIC Source/MetaCoreScripting/Public + PRIVATE third_party +) +target_link_libraries(MetaCoreScripting PUBLIC MetaCoreFoundation MetaCoreScene glm::glm) +if(UNIX AND NOT APPLE) + target_link_libraries(MetaCoreScripting PRIVATE dl) +endif() +target_compile_options(MetaCoreScripting PRIVATE ${METACORE_COMMON_WARNINGS}) + add_library(MetaCoreDelivery STATIC Source/MetaCoreDelivery/Public/MetaCoreDelivery/MetaCoreBuildPipeline.h Source/MetaCoreDelivery/Public/MetaCoreDelivery/MetaCoreCrashReporter.h @@ -489,7 +504,7 @@ target_include_directories(MetaCoreDelivery PRIVATE third_party ) target_link_libraries(MetaCoreDelivery - PUBLIC MetaCoreFoundation MetaCoreScene + PUBLIC MetaCoreFoundation MetaCoreScene MetaCoreScripting ) target_compile_options(MetaCoreDelivery PRIVATE ${METACORE_COMMON_WARNINGS}) if(crashpad_FOUND) @@ -701,12 +716,14 @@ target_link_libraries(MetaCoreEditor MetaCoreRender MetaCoreRuntimeData MetaCoreScene + MetaCoreScripting MetaCoreDelivery glm::glm imgui::imgui ) target_compile_options(MetaCoreEditor PRIVATE ${METACORE_COMMON_WARNINGS}) +target_compile_definitions(MetaCoreEditor PRIVATE METACORE_SOURCE_ROOT="${CMAKE_SOURCE_DIR}") if(WIN32) target_compile_definitions(MetaCoreEditor PRIVATE WIN32_LEAN_AND_MEAN NOMINMAX) endif() @@ -788,6 +805,7 @@ target_link_libraries(MetaCorePlayer MetaCoreRuntimeData MetaCoreScene MetaCoreRuntimeUi + MetaCoreScripting MetaCoreDelivery ) if(UNIX AND NOT APPLE) @@ -809,6 +827,12 @@ if(METACORE_BUILD_TESTS) target_compile_options(MetaCoreDeliveryTests PRIVATE ${METACORE_COMMON_WARNINGS}) add_test(NAME MetaCoreDeliveryTests COMMAND MetaCoreDeliveryTests) + add_executable(MetaCoreScriptingTests tests/MetaCoreScriptingTests.cpp) + target_link_libraries(MetaCoreScriptingTests PRIVATE MetaCoreScripting) + target_compile_options(MetaCoreScriptingTests PRIVATE ${METACORE_COMMON_WARNINGS}) + target_compile_definitions(MetaCoreScriptingTests PRIVATE METACORE_SOURCE_ROOT="${CMAKE_SOURCE_DIR}") + add_test(NAME MetaCoreScriptingTests COMMAND MetaCoreScriptingTests) + add_executable(MetaCoreSmokeTests tests/MetaCoreSmokeTests.cpp Source/MetaCoreRender/Private/MetaCoreGlibcCompat.cpp diff --git a/Source/MetaCoreDelivery/Private/MetaCoreBuildPipeline.cpp b/Source/MetaCoreDelivery/Private/MetaCoreBuildPipeline.cpp index 92f7ef2..9650df9 100644 --- a/Source/MetaCoreDelivery/Private/MetaCoreBuildPipeline.cpp +++ b/Source/MetaCoreDelivery/Private/MetaCoreBuildPipeline.cpp @@ -5,6 +5,7 @@ #include "MetaCoreFoundation/MetaCoreProject.h" #include "MetaCoreScene/MetaCoreScenePackage.h" #include "MetaCoreScene/MetaCoreSceneSerializer.h" +#include "MetaCoreScripting/MetaCoreScripting.h" #include @@ -344,6 +345,11 @@ std::optional MetaCoreReadRuntimeDeliveryDocume document.RuntimeConfig = value->value("runtime_config", ""); document.UiManifest = value->value("ui_manifest", ""); document.ReleaseManifest = value->value("release_manifest", ""); + if (value->contains("script_modules") && (*value)["script_modules"].is_array()) { + for (const auto& module : (*value)["script_modules"]) { + if (module.is_string()) document.ScriptModules.emplace_back(module.get()); + } + } if (document.FormatVersion != GMetaCoreRuntimeDeliveryVersion) { SetIssue(issue, MetaCoreBuildErrorCode::ManifestVersionUnsupported, "Runtime delivery format is unsupported", path); return std::nullopt; @@ -358,16 +364,24 @@ std::optional MetaCoreReadRuntimeDeliveryDocume return std::nullopt; } } + for (const auto& relative : document.ScriptModules) { + if (!IsSafeRelativePath(relative)) { + SetIssue(issue, MetaCoreBuildErrorCode::UnsafePath, "Runtime delivery document contains an unsafe script module path", path); + return std::nullopt; + } + } return document; } bool MetaCoreWriteRuntimeDeliveryDocument(const std::filesystem::path& path, const MetaCoreRuntimeDeliveryDocument& document) { + json modules = json::array(); + for (const auto& module : document.ScriptModules) modules.push_back(module.generic_string()); return WriteJson(path, { {"format_version", document.FormatVersion}, {"runtime_abi_major", document.RuntimeAbiMajor}, {"runtime_abi_minor", document.RuntimeAbiMinor}, {"build_id", document.BuildId}, {"content_root", document.ContentRoot.generic_string()}, {"startup_scene", document.StartupScene.generic_string()}, {"runtime_config", document.RuntimeConfig.generic_string()}, {"ui_manifest", document.UiManifest.generic_string()}, - {"release_manifest", document.ReleaseManifest.generic_string()} + {"release_manifest", document.ReleaseManifest.generic_string()}, {"script_modules", std::move(modules)} }); } @@ -457,7 +471,13 @@ std::vector MetaCoreValidateReleasePackage(const std::filesy MetaCoreBuildIssue deliveryIssue; const auto delivery = MetaCoreReadRuntimeDeliveryDocument(packageRoot / "Project" / "MetaCore.runtimeproject", &deliveryIssue); if (!delivery) issues.push_back(std::move(deliveryIssue)); - else if (delivery->BuildId != manifest->BuildId) issues.push_back({MetaCoreBuildErrorCode::RuntimeAbiIncompatible, "Runtime project and release manifest Build IDs differ", {}, manifest->BuildId, delivery->BuildId}); + else { + if (delivery->BuildId != manifest->BuildId) issues.push_back({MetaCoreBuildErrorCode::RuntimeAbiIncompatible, "Runtime project and release manifest Build IDs differ", {}, manifest->BuildId, delivery->BuildId}); + for (const auto& module : delivery->ScriptModules) { + if (!std::filesystem::is_regular_file(packageRoot / module)) + issues.push_back({MetaCoreBuildErrorCode::RequiredFileMissing, "Required project script module is missing", module, "script_module", "missing"}); + } + } return issues; } @@ -552,6 +572,24 @@ MetaCoreBuildResult MetaCoreBuildPipeline::Run(const MetaCoreBuildRequest& reque result.BuildId = MakeBuildId(); const auto sourceRoot = FindSourceRoot(request.EngineBuildRoot); + std::optional projectScriptModule; + if (std::filesystem::is_regular_file(request.ProjectRoot / "Scripts" / "CMakeLists.txt")) { + report(MetaCoreBuildStage::Build, "Building project script module"); + if (!request.SkipBuild) { + const auto scriptBuild = MetaCoreBuildScriptProject( + request.ProjectRoot, CMakeConfigurationName(profile->Configuration), sourceRoot + ); + if (!scriptBuild.Success) { + return fail(MetaCoreBuildErrorCode::BuildFailed, "Project script module build failed: " + scriptBuild.Output, request.ProjectRoot / "Scripts"); + } + projectScriptModule = scriptBuild.ModulePath; + } else { + projectScriptModule = MetaCoreFindLatestScriptModule(request.ProjectRoot, CMakeConfigurationName(profile->Configuration)); + if (!projectScriptModule.has_value()) { + return fail(MetaCoreBuildErrorCode::RequiredFileMissing, "Project script module has not been built", request.ProjectRoot / "Scripts"); + } + } + } const auto outputRoot = request.ProjectRoot / profile->OutputDirectory / profile->TargetPlatform / project->Name / profile->Name; const std::string packageName = project->Name + "-" + project->Version + "-" + result.BuildId; const auto staging = outputRoot / (".staging-" + result.BuildId); @@ -571,6 +609,7 @@ MetaCoreBuildResult MetaCoreBuildPipeline::Run(const MetaCoreBuildRequest& reque std::filesystem::create_directories(staging / "Engine" / "Materials", error); std::filesystem::create_directories(staging / "Engine" / "Fonts", error); std::filesystem::create_directories(staging / "Diagnostics" / "Logs", error); + std::filesystem::create_directories(staging / "Project" / "Scripts", error); if (error) return fail(MetaCoreBuildErrorCode::StageFailed, "Failed to create staging directory", staging); report(MetaCoreBuildStage::Cook, "Cooking startup scene and project content"); @@ -609,6 +648,18 @@ MetaCoreBuildResult MetaCoreBuildPipeline::Run(const MetaCoreBuildRequest& reque std::filesystem::copy_file(player, staging / player.filename(), std::filesystem::copy_options::overwrite_existing, error); if (error) return fail(MetaCoreBuildErrorCode::StageFailed, "Failed to stage MetaCorePlayer", player); std::filesystem::permissions(staging / player.filename(), std::filesystem::perms::owner_exec | std::filesystem::perms::group_exec | std::filesystem::perms::others_exec, std::filesystem::perm_options::add, error); + std::filesystem::path stagedScriptModule; + if (projectScriptModule.has_value()) { + stagedScriptModule = staging / "Project" / "Scripts" / projectScriptModule->filename(); + std::filesystem::copy_file(*projectScriptModule, stagedScriptModule, std::filesystem::copy_options::overwrite_existing, error); + if (error) return fail(MetaCoreBuildErrorCode::StageFailed, "Failed to stage project script module", *projectScriptModule); + std::filesystem::path scriptManifest = *projectScriptModule; + scriptManifest += ".mcscriptmodule.json"; + if (!std::filesystem::is_regular_file(scriptManifest)) + return fail(MetaCoreBuildErrorCode::RequiredFileMissing, "Project script module manifest is missing", scriptManifest); + std::filesystem::copy_file(scriptManifest, staging / "Project" / "Scripts" / scriptManifest.filename(), std::filesystem::copy_options::overwrite_existing, error); + if (error) return fail(MetaCoreBuildErrorCode::StageFailed, "Failed to stage project script module manifest", scriptManifest); + } for (const char* material : {"uiBlit.filamat", "rml_ui.filamat"}) { const auto source = request.EngineBuildRoot / material; if (!std::filesystem::is_regular_file(source)) return fail(MetaCoreBuildErrorCode::StageFailed, "Required engine material is missing", source); @@ -647,6 +698,7 @@ MetaCoreBuildResult MetaCoreBuildPipeline::Run(const MetaCoreBuildRequest& reque MetaCoreRuntimeDeliveryDocument delivery; delivery.BuildId = result.BuildId; delivery.StartupScene = std::filesystem::path("Content") / "Scenes" / cookedSceneName; + if (!stagedScriptModule.empty()) delivery.ScriptModules.push_back(std::filesystem::path("Project") / "Scripts" / stagedScriptModule.filename()); if (!MetaCoreWriteRuntimeDeliveryDocument(staging / "Project" / "MetaCore.runtimeproject", delivery)) return fail(MetaCoreBuildErrorCode::StageFailed, "Failed to write runtime project document", staging); @@ -662,6 +714,26 @@ MetaCoreBuildResult MetaCoreBuildPipeline::Run(const MetaCoreBuildRequest& reque !RunCommand("objcopy --add-gnu-debuglink=" + ShellQuote(debugFile) + " " + ShellQuote(staging / player.filename()))) return fail(MetaCoreBuildErrorCode::PackageFailed, "Failed to strip or link release symbols", staging / player.filename()); } + if (!stagedScriptModule.empty()) { + const auto scriptDebug = symbolsRoot / (stagedScriptModule.filename().string() + ".debug"); + if (!RunCommand("objcopy --only-keep-debug " + ShellQuote(stagedScriptModule) + " " + ShellQuote(scriptDebug))) + return fail(MetaCoreBuildErrorCode::PackageFailed, "Failed to create project script debug symbols", stagedScriptModule); + if (profile->Configuration == MetaCoreBuildConfiguration::Release && + (!RunCommand("strip --strip-unneeded " + ShellQuote(stagedScriptModule)) || + !RunCommand("objcopy --add-gnu-debuglink=" + ShellQuote(scriptDebug) + " " + ShellQuote(stagedScriptModule)))) + return fail(MetaCoreBuildErrorCode::PackageFailed, "Failed to strip project script module", stagedScriptModule); + if (profile->Configuration == MetaCoreBuildConfiguration::Release) { + std::filesystem::path scriptManifest = stagedScriptModule; + scriptManifest += ".mcscriptmodule.json"; + auto manifestJson = ReadJson(scriptManifest); + const auto strippedHash = MetaCoreSha256File(stagedScriptModule); + if (!manifestJson.has_value() || !strippedHash.has_value()) + return fail(MetaCoreBuildErrorCode::PackageFailed, "Failed to refresh stripped project script manifest", scriptManifest); + (*manifestJson)["sha256"] = *strippedHash; + if (!WriteJson(scriptManifest, *manifestJson)) + return fail(MetaCoreBuildErrorCode::PackageFailed, "Failed to write stripped project script manifest", scriptManifest); + } + } } report(MetaCoreBuildStage::Package, "Writing release manifest and archives"); diff --git a/Source/MetaCoreDelivery/Public/MetaCoreDelivery/MetaCoreBuildPipeline.h b/Source/MetaCoreDelivery/Public/MetaCoreDelivery/MetaCoreBuildPipeline.h index 1872786..44c509b 100644 --- a/Source/MetaCoreDelivery/Public/MetaCoreDelivery/MetaCoreBuildPipeline.h +++ b/Source/MetaCoreDelivery/Public/MetaCoreDelivery/MetaCoreBuildPipeline.h @@ -61,6 +61,7 @@ struct MetaCoreRuntimeDeliveryDocument { std::filesystem::path RuntimeConfig = "Content/Runtime/ProjectRuntime.mcruntimecfg"; std::filesystem::path UiManifest = "Content/Runtime/Ui.mcruntime"; std::filesystem::path ReleaseManifest = "Manifest/MetaCore.release.json"; + std::vector ScriptModules{}; }; struct MetaCoreReleaseFileEntry { diff --git a/Source/MetaCoreEditor/Private/MetaCoreBuiltinCoreServicesModule.cpp b/Source/MetaCoreEditor/Private/MetaCoreBuiltinCoreServicesModule.cpp index dfba09a..b386709 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreBuiltinCoreServicesModule.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreBuiltinCoreServicesModule.cpp @@ -41,6 +41,7 @@ #include #include #include +#include #include #include #include @@ -690,12 +691,18 @@ void MetaCoreCopyStringToBuffer(const std::string& value, char* buffer, std::siz switch (field.Type) { case MetaCoreScriptFieldType::Bool: return field.BoolDefault; + case MetaCoreScriptFieldType::Int: + return field.IntDefault; case MetaCoreScriptFieldType::Float: return field.FloatDefault; case MetaCoreScriptFieldType::Vec3: return nlohmann::json::array({field.Vec3Default.x, field.Vec3Default.y, field.Vec3Default.z}); case MetaCoreScriptFieldType::String: return field.StringDefault; + case MetaCoreScriptFieldType::AssetRef: + return field.AssetDefault.IsValid() ? field.AssetDefault.ToString() : field.StringDefault; + case MetaCoreScriptFieldType::GameObjectRef: + return field.GameObjectDefault; } return nullptr; } @@ -3119,6 +3126,38 @@ void MetaCoreDrawScriptComponentInspector(MetaCoreEditorContext& editorContext, return; } + if (const auto projectScripts = editorContext.GetModuleRegistry().ResolveService(); + projectScripts != nullptr) { + static std::array newBehaviourName{'N','e','w','B','e','h','a','v','i','o','u','r','\0'}; + if (ImGui::Button("生成 C++ 脚本工程")) { + (void)projectScripts->GenerateProject(); + } + ImGui::SameLine(); + const MetaCoreProjectScriptStatus beforeStatus = projectScripts->GetStatus(); + ImGui::BeginDisabled(beforeStatus.State == MetaCoreProjectScriptBuildState::Building); + if (ImGui::Button("编译并加载脚本")) { + (void)projectScripts->CompileAsync("Debug"); + } + ImGui::EndDisabled(); + ImGui::SetNextItemWidth(180.0F); + ImGui::InputText("行为名##NewScriptBehaviour", newBehaviourName.data(), newBehaviourName.size()); + ImGui::SameLine(); + if (ImGui::Button("创建 C++ 行为")) { + (void)projectScripts->CreateBehaviour(newBehaviourName.data()); + } + const MetaCoreProjectScriptStatus status = projectScripts->GetStatus(); + if (status.State != MetaCoreProjectScriptBuildState::Idle && !status.Message.empty()) { + const ImVec4 color = status.State == MetaCoreProjectScriptBuildState::Failed + ? ImVec4(0.95F, 0.35F, 0.30F, 1.0F) + : status.State == MetaCoreProjectScriptBuildState::PendingActivation + ? ImVec4(0.95F, 0.70F, 0.25F, 1.0F) + : ImVec4(0.55F, 0.85F, 0.60F, 1.0F); + const std::string summary = status.Message.substr(0, 600); + ImGui::TextColored(color, "%s", summary.c_str()); + } + ImGui::Separator(); + } + const std::vector scriptDefinitions = scriptRegistry->GetScriptDefinitions(); if (scriptDefinitions.empty()) { ImGui::TextDisabled("当前没有已注册脚本类型"); @@ -3204,40 +3243,51 @@ void MetaCoreDrawScriptComponentInspector(MetaCoreEditorContext& editorContext, nlohmann::json fields = MetaCoreParseScriptFieldsJson(instance.FieldsJson); for (const MetaCoreScriptFieldDefinition& field : definition->Fields) { - if (field.Name.empty()) { + const std::string fieldId(field.StableId()); + if (fieldId.empty()) { continue; } const std::string labelPrefix = field.DisplayName.empty() ? field.Name : field.DisplayName; - const std::string label = labelPrefix + "##" + field.Name; + const std::string label = labelPrefix + "##" + fieldId; bool fieldChanged = false; switch (field.Type) { case MetaCoreScriptFieldType::Bool: { - bool value = MetaCoreReadScriptBoolField(fields, field.Name, field.BoolDefault); + bool value = MetaCoreReadScriptBoolField(fields, fieldId, field.BoolDefault); ImGui::Checkbox(label.c_str(), &value); MetaCoreTrackComponentInspectorEdit(editorContext, "修改检查器属性", false); if (ImGui::IsItemEdited()) { - fields[field.Name] = value; + fields[fieldId] = value; + fieldChanged = true; + } + break; + } + case MetaCoreScriptFieldType::Int: { + std::int64_t value = fields.value(fieldId, field.IntDefault); + ImGui::InputScalar(label.c_str(), ImGuiDataType_S64, &value); + MetaCoreTrackComponentInspectorEdit(editorContext, "修改检查器属性", true); + if (ImGui::IsItemEdited()) { + fields[fieldId] = value; fieldChanged = true; } break; } case MetaCoreScriptFieldType::Float: { - float value = MetaCoreReadScriptFloatField(fields, field.Name, field.FloatDefault); + float value = MetaCoreReadScriptFloatField(fields, fieldId, field.FloatDefault); ImGui::DragFloat(label.c_str(), &value, 0.1F); MetaCoreTrackComponentInspectorEdit(editorContext, "修改检查器属性", true); if (ImGui::IsItemEdited()) { - fields[field.Name] = value; + fields[fieldId] = value; fieldChanged = true; } break; } case MetaCoreScriptFieldType::Vec3: { - glm::vec3 value = MetaCoreReadScriptVec3Field(fields, field.Name, field.Vec3Default); + glm::vec3 value = MetaCoreReadScriptVec3Field(fields, fieldId, field.Vec3Default); ImGui::DragFloat3(label.c_str(), glm::value_ptr(value), 0.05F); MetaCoreTrackComponentInspectorEdit(editorContext, "修改检查器属性", true); if (ImGui::IsItemEdited()) { - fields[field.Name] = nlohmann::json::array({value.x, value.y, value.z}); + fields[fieldId] = nlohmann::json::array({value.x, value.y, value.z}); fieldChanged = true; } break; @@ -3245,18 +3295,45 @@ void MetaCoreDrawScriptComponentInspector(MetaCoreEditorContext& editorContext, case MetaCoreScriptFieldType::String: { std::array buffer{}; MetaCoreCopyStringToBuffer( - MetaCoreReadScriptStringField(fields, field.Name, field.StringDefault), + MetaCoreReadScriptStringField(fields, fieldId, field.StringDefault), buffer.data(), buffer.size() ); ImGui::InputText(label.c_str(), buffer.data(), buffer.size()); MetaCoreTrackComponentInspectorEdit(editorContext, "修改检查器属性", true); if (ImGui::IsItemEdited()) { - fields[field.Name] = std::string(buffer.data()); + fields[fieldId] = std::string(buffer.data()); fieldChanged = true; } break; } + case MetaCoreScriptFieldType::AssetRef: { + std::array buffer{}; + MetaCoreCopyStringToBuffer(MetaCoreReadScriptStringField(fields, fieldId, field.StringDefault), buffer.data(), buffer.size()); + ImGui::InputText(label.c_str(), buffer.data(), buffer.size()); + MetaCoreTrackComponentInspectorEdit(editorContext, "修改资源引用", false); + if (ImGui::IsItemEdited()) { + fields[fieldId] = std::string(buffer.data()); + fieldChanged = true; + } + if (buffer[0] != '\0' && !MetaCoreAssetGuid::Parse(buffer.data()).has_value()) { + ImGui::TextColored(ImVec4(0.95F, 0.55F, 0.20F, 1.0F), "资源 GUID 无效或已失效"); + } + break; + } + case MetaCoreScriptFieldType::GameObjectRef: { + std::uint64_t value = fields.value(fieldId, field.GameObjectDefault); + ImGui::InputScalar(label.c_str(), ImGuiDataType_U64, &value); + MetaCoreTrackComponentInspectorEdit(editorContext, "修改对象引用", false); + if (ImGui::IsItemEdited()) { + fields[fieldId] = value; + fieldChanged = true; + } + if (value != 0 && !editorContext.GetScene().FindGameObject(value)) { + ImGui::TextColored(ImVec4(0.95F, 0.55F, 0.20F, 1.0F), "对象引用已失效,原始 ID 已保留"); + } + break; + } } if (fieldChanged) { @@ -9999,7 +10076,7 @@ public: context.GameObject.GetComponent().RotationEulerDegrees.y += degreesPerSecond * static_cast(deltaSeconds); - context.EditorContext.GetScene().IncrementRevision(); + context.Scene.IncrementRevision(); } }; @@ -10024,7 +10101,7 @@ public: context.GameObject.GetComponent().Position += velocity * static_cast(deltaSeconds); - context.EditorContext.GetScene().IncrementRevision(); + context.Scene.IncrementRevision(); } }; @@ -10034,7 +10111,7 @@ public: void Startup(MetaCoreEditorModuleRegistry& moduleRegistry) override { (void)moduleRegistry; - Definitions_.clear(); + Registry_.Clear(); MetaCoreScriptDefinition rotator; rotator.TypeId = "MetaCore.Script.Rotator"; @@ -10091,57 +10168,177 @@ public: void Shutdown(MetaCoreEditorModuleRegistry& moduleRegistry) override { (void)moduleRegistry; - Definitions_.clear(); + Registry_.Clear(); } void RegisterScript(MetaCoreScriptDefinition definition) override { - if (definition.TypeId.empty()) { - return; - } - - const auto iterator = std::find_if(Definitions_.begin(), Definitions_.end(), [&](const MetaCoreScriptDefinition& existing) { - return existing.TypeId == definition.TypeId; - }); - if (iterator != Definitions_.end()) { - *iterator = std::move(definition); - return; - } - Definitions_.push_back(std::move(definition)); + (void)Registry_.RegisterScript(std::move(definition)); } [[nodiscard]] const MetaCoreScriptDefinition* FindScript(std::string_view typeId) const override { - const auto iterator = std::find_if(Definitions_.begin(), Definitions_.end(), [&](const MetaCoreScriptDefinition& definition) { - return definition.TypeId == typeId; - }); - return iterator == Definitions_.end() ? nullptr : &(*iterator); + return Registry_.FindScript(typeId); } [[nodiscard]] std::vector GetScriptDefinitions() const override { - return Definitions_; + return Registry_.GetScriptDefinitions(); } [[nodiscard]] std::unique_ptr CreateBehaviour(std::string_view typeId) const override { - const MetaCoreScriptDefinition* definition = FindScript(typeId); - if (definition == nullptr || !definition->Factory) { - return nullptr; - } - return definition->Factory(); + return Registry_.CreateBehaviour(typeId); } [[nodiscard]] std::string BuildDefaultFieldsJson(std::string_view typeId) const override { - const MetaCoreScriptDefinition* definition = FindScript(typeId); - return definition != nullptr ? MetaCoreBuildDefaultScriptFieldsJson(*definition) : std::string("{}"); + return Registry_.BuildDefaultFieldsJson(typeId); } [[nodiscard]] std::string EnsureFieldsJsonDefaults(std::string_view typeId, std::string_view fieldsJson) const override { - const MetaCoreScriptDefinition* definition = FindScript(typeId); - return definition != nullptr - ? MetaCoreEnsureScriptFieldsJsonDefaults(*definition, fieldsJson) - : MetaCoreParseScriptFieldsJson(fieldsJson).dump(); + return Registry_.EnsureFieldsJsonDefaults(typeId, fieldsJson); } + [[nodiscard]] MetaCoreScriptRegistry& GetRuntimeRegistry() override { return Registry_; } + [[nodiscard]] const MetaCoreScriptRegistry& GetRuntimeRegistry() const override { return Registry_; } + private: - std::vector Definitions_{}; + MetaCoreScriptRegistry Registry_{}; +}; + +class MetaCoreBuiltinProjectScriptService final : public MetaCoreIProjectScriptService { +public: + [[nodiscard]] std::string GetServiceId() const override { return "MetaCore.ProjectScriptService"; } + + void Startup(MetaCoreEditorModuleRegistry& moduleRegistry) override { + AssetDatabase_ = moduleRegistry.ResolveService(); + Registry_ = moduleRegistry.ResolveService(); + TryLoadExisting(); + } + + void Shutdown(MetaCoreEditorModuleRegistry&) override { + if (BuildFuture_.valid()) { + BuildFuture_.wait(); + } + Pending_.reset(); + Loaded_.reset(); + Registry_.reset(); + AssetDatabase_.reset(); + } + + [[nodiscard]] bool GenerateProject() override { + if (AssetDatabase_ == nullptr || !AssetDatabase_->HasProject()) { + Status_ = {MetaCoreProjectScriptBuildState::Failed, "没有已打开的项目", {}}; + return false; + } + std::string error; + const auto& project = AssetDatabase_->GetProjectDescriptor(); + const bool generated = MetaCoreGenerateScriptProject( + project.RootPath, project.Name, std::filesystem::path(METACORE_SOURCE_ROOT), &error + ); + Status_ = generated + ? MetaCoreProjectScriptStatus{MetaCoreProjectScriptBuildState::Succeeded, "脚本工程已生成", project.RootPath / "Scripts"} + : MetaCoreProjectScriptStatus{MetaCoreProjectScriptBuildState::Failed, std::move(error), {}}; + return generated; + } + + [[nodiscard]] bool CompileAsync(std::string_view configuration) override { + if (BuildFuture_.valid() && BuildFuture_.wait_for(std::chrono::seconds(0)) != std::future_status::ready) { + return false; + } + if (AssetDatabase_ == nullptr || !AssetDatabase_->HasProject()) { + Status_ = {MetaCoreProjectScriptBuildState::Failed, "没有已打开的项目", {}}; + return false; + } + const auto root = AssetDatabase_->GetProjectDescriptor().RootPath; + if (!std::filesystem::is_regular_file(root / "Scripts" / "CMakeLists.txt") && !GenerateProject()) { + return false; + } + const std::string config(configuration.empty() ? "Debug" : configuration); + Status_ = {MetaCoreProjectScriptBuildState::Building, "正在后台编译项目脚本…", {}}; + BuildFuture_ = std::async(std::launch::async, [root, config] { + return MetaCoreBuildScriptProject(root, config, std::filesystem::path(METACORE_SOURCE_ROOT)); + }); + return true; + } + + [[nodiscard]] bool CreateBehaviour(std::string_view behaviourName) override { + if (AssetDatabase_ == nullptr || !AssetDatabase_->HasProject()) { + Status_ = {MetaCoreProjectScriptBuildState::Failed, "没有已打开的项目", {}}; + return false; + } + const auto& project = AssetDatabase_->GetProjectDescriptor(); + if (!std::filesystem::is_regular_file(project.RootPath / "Scripts" / "CMakeLists.txt") && !GenerateProject()) return false; + std::string error; + const bool created = MetaCoreCreateScriptBehaviour(project.RootPath, project.Name, behaviourName, &error); + Status_ = created + ? MetaCoreProjectScriptStatus{MetaCoreProjectScriptBuildState::Succeeded, "已创建行为源码: " + std::string(behaviourName), project.RootPath / "Scripts" / "Source"} + : MetaCoreProjectScriptStatus{MetaCoreProjectScriptBuildState::Failed, std::move(error), {}}; + return created; + } + + void Tick(bool canActivate) override { + if (AssetDatabase_ != nullptr && AssetDatabase_->HasProject()) { + const auto currentRoot = AssetDatabase_->GetProjectDescriptor().RootPath; + if (ActiveProjectRoot_ != currentRoot) { + if (Loaded_.has_value() && Registry_ != nullptr) Registry_->GetRuntimeRegistry().RemoveModule(Loaded_->ModuleId); + Loaded_.reset(); + Pending_.reset(); + ActiveProjectRoot_ = currentRoot; + TryLoadExisting(); + } + } + if (BuildFuture_.valid() && BuildFuture_.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { + MetaCoreScriptProjectBuildResult result = BuildFuture_.get(); + if (!result.Success) { + Status_ = {MetaCoreProjectScriptBuildState::Failed, result.Output, result.ModulePath}; + } else { + std::string error; + auto candidate = Loader_.LoadCandidate(result.ModulePath, &error); + if (!candidate.has_value()) { + Status_ = {MetaCoreProjectScriptBuildState::Failed, "模块加载失败: " + error, result.ModulePath}; + } else { + Pending_ = std::move(*candidate); + Status_ = {MetaCoreProjectScriptBuildState::PendingActivation, + canActivate ? "模块验证完成,等待提交" : "播放中:新模块将在 Stop 后启用", result.ModulePath}; + } + } + } + if (canActivate && Pending_.has_value() && Registry_ != nullptr) { + std::string error; + if (Registry_->GetRuntimeRegistry().ReplaceModule(Pending_->ModuleId, Pending_->Definitions, &error)) { + Loaded_ = std::move(Pending_); + Pending_.reset(); + Status_ = {MetaCoreProjectScriptBuildState::Succeeded, "脚本模块已加载: " + Loaded_->ModuleId, Loaded_->Path}; + } else { + Status_ = {MetaCoreProjectScriptBuildState::Failed, "脚本注册失败,旧模块保持不变: " + error, Pending_->Path}; + Pending_.reset(); + } + } + } + + [[nodiscard]] MetaCoreProjectScriptStatus GetStatus() const override { return Status_; } + +private: + void TryLoadExisting() { + if (AssetDatabase_ == nullptr || Registry_ == nullptr || !AssetDatabase_->HasProject()) return; + ActiveProjectRoot_ = AssetDatabase_->GetProjectDescriptor().RootPath; + const auto module = MetaCoreFindLatestScriptModule(AssetDatabase_->GetProjectDescriptor().RootPath); + if (!module.has_value()) return; + std::string error; + auto candidate = Loader_.LoadCandidate(*module, &error); + if (candidate.has_value() && Registry_->GetRuntimeRegistry().ReplaceModule(candidate->ModuleId, candidate->Definitions, &error)) { + Loaded_ = std::move(candidate); + Status_ = {MetaCoreProjectScriptBuildState::Succeeded, "已加载现有脚本模块: " + Loaded_->ModuleId, Loaded_->Path}; + } else { + Status_ = {MetaCoreProjectScriptBuildState::Failed, "现有脚本模块加载失败: " + error, *module}; + } + } + + std::shared_ptr AssetDatabase_{}; + std::shared_ptr Registry_{}; + MetaCoreScriptModuleLoader Loader_{}; + std::future BuildFuture_{}; + std::optional Pending_{}; + std::optional Loaded_{}; + MetaCoreProjectScriptStatus Status_{}; + std::filesystem::path ActiveProjectRoot_{}; }; class MetaCoreScriptPlayModeComponentSystem final : public MetaCoreIPlayModeComponentSystem { @@ -10149,289 +10346,37 @@ public: [[nodiscard]] std::string GetSystemId() const override { return "MetaCore.PlayMode.Script"; } void OnPlayStart(MetaCoreEditorContext& editorContext) override { - MetaCoreDebugLog( - "ScriptSystem", - "OnPlayStart begin runtimeEntries=" + std::to_string(RuntimeEntries_.size()) + " " + - MetaCoreDebugSceneSummary(editorContext) - ); - RuntimeEntries_.clear(); - MissingScriptWarnings_.clear(); - DispatchScriptPhase(editorContext, 0.0, ScriptPhase::PlayStart); - MetaCoreDebugLog( - "ScriptSystem", - "OnPlayStart end runtimeEntries=" + std::to_string(RuntimeEntries_.size()) + " " + - MetaCoreDebugSceneSummary(editorContext) - ); - } - - void FixedUpdate(MetaCoreEditorContext& editorContext, double fixedDeltaSeconds) override { - DispatchScriptPhase(editorContext, fixedDeltaSeconds, ScriptPhase::FixedUpdate); - } - - void Update(MetaCoreEditorContext& editorContext, double deltaSeconds) override { - DispatchScriptPhase(editorContext, deltaSeconds, ScriptPhase::Update); - } - - void LateUpdate(MetaCoreEditorContext& editorContext, double deltaSeconds) override { - DispatchScriptPhase(editorContext, deltaSeconds, ScriptPhase::LateUpdate); - } - - void OnPlayStop(MetaCoreEditorContext& editorContext) override { - MetaCoreDebugLog( - "ScriptSystem", - "OnPlayStop begin runtimeEntries=" + std::to_string(RuntimeEntries_.size()) + " " + - MetaCoreDebugSceneSummary(editorContext) - ); - for (RuntimeEntry& entry : RuntimeEntries_) { - if (entry.Behaviour == nullptr || !entry.Started) { - continue; + const auto registry = editorContext.GetModuleRegistry().ResolveService(); + if (registry == nullptr) return; + Runtime_ = std::make_unique( + editorContext.GetScene(), registry->GetRuntimeRegistry(), + [&editorContext](std::uint32_t level, std::string_view category, std::string_view message) { + const MetaCoreLogLevel mapped = level >= 2U ? MetaCoreLogLevel::Error : level == 1U ? MetaCoreLogLevel::Warning : MetaCoreLogLevel::Info; + editorContext.AddConsoleMessage(mapped, std::string(category), std::string(message)); } - - MetaCoreGameObject gameObject = editorContext.GetScene().FindGameObject(entry.ObjectId); - MetaCoreScriptInstanceData* instance = FindScriptInstance(gameObject, entry.InstanceId); - if (gameObject && instance != nullptr) { - MetaCoreDebugLog( - "ScriptSystem", - "OnPlayStop dispatch " + MetaCoreDebugScriptIdentity(gameObject, *instance) - ); - MetaCoreScriptExecutionContext scriptContext{editorContext, gameObject, *instance}; - entry.Behaviour->OnPlayStop(scriptContext); - } else { - std::ostringstream stream; - stream - << "OnPlayStop skip missing target objectId=" << entry.ObjectId - << " instanceId=" << entry.InstanceId - << " type=\"" << entry.ScriptTypeId << "\""; - MetaCoreDebugLog("ScriptSystem", stream.str()); - } - } - RuntimeEntries_.clear(); - MissingScriptWarnings_.clear(); - MetaCoreDebugLog( - "ScriptSystem", - "OnPlayStop end runtimeEntries=" + std::to_string(RuntimeEntries_.size()) + " " + - MetaCoreDebugSceneSummary(editorContext) ); + Runtime_->Start(); + } + + void FixedUpdate(MetaCoreEditorContext&, double fixedDeltaSeconds) override { + if (Runtime_ != nullptr) Runtime_->FixedUpdate(fixedDeltaSeconds); + } + + void Update(MetaCoreEditorContext&, double deltaSeconds) override { + if (Runtime_ != nullptr) Runtime_->Update(deltaSeconds); + } + + void LateUpdate(MetaCoreEditorContext&, double deltaSeconds) override { + if (Runtime_ != nullptr) Runtime_->LateUpdate(deltaSeconds); + } + + void OnPlayStop(MetaCoreEditorContext&) override { + if (Runtime_ != nullptr) Runtime_->Stop(); + Runtime_.reset(); } private: - enum class ScriptPhase { - PlayStart, - FixedUpdate, - Update, - LateUpdate - }; - - struct RuntimeEntry { - MetaCoreId ObjectId = 0; - std::uint64_t InstanceId = 0; - std::string ScriptTypeId{}; - std::unique_ptr Behaviour{}; - bool Started = false; - }; - - [[nodiscard]] static MetaCoreScriptInstanceData* FindScriptInstance(MetaCoreGameObject& gameObject, std::uint64_t instanceId) { - if (!gameObject || !gameObject.HasComponent()) { - return nullptr; - } - - MetaCoreScriptComponent& scriptComponent = gameObject.GetComponent(); - const auto iterator = std::find_if(scriptComponent.Instances.begin(), scriptComponent.Instances.end(), [&](MetaCoreScriptInstanceData& instance) { - return instance.InstanceId == instanceId; - }); - return iterator == scriptComponent.Instances.end() ? nullptr : &(*iterator); - } - - [[nodiscard]] RuntimeEntry* FindRuntimeEntry(MetaCoreId objectId, std::uint64_t instanceId) { - const auto iterator = std::find_if(RuntimeEntries_.begin(), RuntimeEntries_.end(), [&](const RuntimeEntry& entry) { - return entry.ObjectId == objectId && entry.InstanceId == instanceId; - }); - return iterator == RuntimeEntries_.end() ? nullptr : &(*iterator); - } - - [[nodiscard]] RuntimeEntry* EnsureRuntimeEntry( - MetaCoreEditorContext& editorContext, - MetaCoreGameObject& gameObject, - MetaCoreScriptInstanceData& instance - ) { - const std::uint64_t instanceId = MetaCoreEnsureScriptInstanceId(instance); - if (instance.ScriptTypeId.empty()) { - WarnMissingScript(editorContext, ""); - return nullptr; - } - - if (RuntimeEntry* existingEntry = FindRuntimeEntry(gameObject.GetId(), instanceId); existingEntry != nullptr) { - if (existingEntry->ScriptTypeId == instance.ScriptTypeId) { - return existingEntry; - } - MetaCoreDebugLog( - "ScriptSystem", - "ReplaceRuntimeEntry " + MetaCoreDebugScriptIdentity(gameObject, instance) + - " oldType=\"" + existingEntry->ScriptTypeId + "\"" - ); - StopRuntimeEntry(editorContext, *existingEntry); - RuntimeEntries_.erase(std::remove_if(RuntimeEntries_.begin(), RuntimeEntries_.end(), [&](const RuntimeEntry& entry) { - return entry.ObjectId == gameObject.GetId() && entry.InstanceId == instanceId; - }), RuntimeEntries_.end()); - } - - const auto scriptRegistry = editorContext.GetModuleRegistry().ResolveService(); - if (scriptRegistry == nullptr || scriptRegistry->FindScript(instance.ScriptTypeId) == nullptr) { - WarnMissingScript(editorContext, instance.ScriptTypeId); - return nullptr; - } - - std::unique_ptr behaviour = scriptRegistry->CreateBehaviour(instance.ScriptTypeId); - if (behaviour == nullptr) { - WarnMissingScript(editorContext, instance.ScriptTypeId); - return nullptr; - } - - instance.FieldsJson = scriptRegistry->EnsureFieldsJsonDefaults(instance.ScriptTypeId, instance.FieldsJson); - - MetaCoreDebugLog( - "ScriptSystem", - "CreateRuntimeEntry begin " + MetaCoreDebugScriptIdentity(gameObject, instance) + - " fields=" + instance.FieldsJson - ); - RuntimeEntry entry; - entry.ObjectId = gameObject.GetId(); - entry.InstanceId = instanceId; - entry.ScriptTypeId = instance.ScriptTypeId; - entry.Behaviour = std::move(behaviour); - RuntimeEntries_.push_back(std::move(entry)); - - RuntimeEntry& createdEntry = RuntimeEntries_.back(); - MetaCoreScriptExecutionContext scriptContext{editorContext, gameObject, instance}; - createdEntry.Behaviour->OnPlayStart(scriptContext); - createdEntry.Started = true; - MetaCoreDebugLog( - "ScriptSystem", - "CreateRuntimeEntry end " + MetaCoreDebugScriptIdentity(gameObject, instance) + - " runtimeEntries=" + std::to_string(RuntimeEntries_.size()) - ); - return &createdEntry; - } - - void StopRuntimeEntry(MetaCoreEditorContext& editorContext, RuntimeEntry& entry) { - if (entry.Behaviour == nullptr || !entry.Started) { - std::ostringstream stream; - stream - << "StopRuntimeEntry skip objectId=" << entry.ObjectId - << " instanceId=" << entry.InstanceId - << " type=\"" << entry.ScriptTypeId << "\"" - << " started=" << (entry.Started ? "true" : "false"); - MetaCoreDebugLog("ScriptSystem", stream.str()); - return; - } - - MetaCoreGameObject gameObject = editorContext.GetScene().FindGameObject(entry.ObjectId); - MetaCoreScriptInstanceData* instance = FindScriptInstance(gameObject, entry.InstanceId); - if (gameObject && instance != nullptr) { - MetaCoreDebugLog( - "ScriptSystem", - "StopRuntimeEntry dispatch " + MetaCoreDebugScriptIdentity(gameObject, *instance) - ); - MetaCoreScriptExecutionContext scriptContext{editorContext, gameObject, *instance}; - entry.Behaviour->OnPlayStop(scriptContext); - } else { - std::ostringstream stream; - stream - << "StopRuntimeEntry missing target objectId=" << entry.ObjectId - << " instanceId=" << entry.InstanceId - << " type=\"" << entry.ScriptTypeId << "\""; - MetaCoreDebugLog("ScriptSystem", stream.str()); - } - entry.Started = false; - } - - void CleanupStaleRuntimeEntries(MetaCoreEditorContext& editorContext) { - auto iterator = RuntimeEntries_.begin(); - while (iterator != RuntimeEntries_.end()) { - MetaCoreGameObject gameObject = editorContext.GetScene().FindGameObject(iterator->ObjectId); - MetaCoreScriptInstanceData* instance = FindScriptInstance(gameObject, iterator->InstanceId); - const bool stale = - !gameObject || - instance == nullptr || - !instance->Enabled || - instance->ScriptTypeId != iterator->ScriptTypeId; - if (stale) { - std::ostringstream stream; - stream - << "CleanupStaleRuntimeEntry objectId=" << iterator->ObjectId - << " instanceId=" << iterator->InstanceId - << " type=\"" << iterator->ScriptTypeId << "\"" - << " hasObject=" << (gameObject ? "true" : "false") - << " hasInstance=" << (instance != nullptr ? "true" : "false"); - if (instance != nullptr) { - stream - << " enabled=" << (instance->Enabled ? "true" : "false") - << " currentType=\"" << instance->ScriptTypeId << "\""; - } - MetaCoreDebugLog("ScriptSystem", stream.str()); - StopRuntimeEntry(editorContext, *iterator); - iterator = RuntimeEntries_.erase(iterator); - } else { - ++iterator; - } - } - } - - void DispatchScriptPhase(MetaCoreEditorContext& editorContext, double deltaSeconds, ScriptPhase phase) { - CleanupStaleRuntimeEntries(editorContext); - - for (MetaCoreGameObject gameObject : editorContext.GetScene().GetGameObjects()) { - if (!gameObject.HasComponent()) { - continue; - } - - MetaCoreScriptComponent& scriptComponent = gameObject.GetComponent(); - for (MetaCoreScriptInstanceData& instance : scriptComponent.Instances) { - if (!instance.Enabled) { - continue; - } - - RuntimeEntry* entry = EnsureRuntimeEntry(editorContext, gameObject, instance); - if (entry == nullptr || entry->Behaviour == nullptr) { - continue; - } - - if (phase == ScriptPhase::PlayStart) { - continue; - } - - MetaCoreScriptExecutionContext scriptContext{editorContext, gameObject, instance}; - switch (phase) { - case ScriptPhase::FixedUpdate: - entry->Behaviour->FixedUpdate(scriptContext, deltaSeconds); - break; - case ScriptPhase::Update: - entry->Behaviour->Update(scriptContext, deltaSeconds); - break; - case ScriptPhase::LateUpdate: - entry->Behaviour->LateUpdate(scriptContext, deltaSeconds); - break; - case ScriptPhase::PlayStart: - break; - } - } - } - } - - void WarnMissingScript(MetaCoreEditorContext& editorContext, std::string_view scriptTypeId) { - const std::string warningKey(scriptTypeId); - if (!MissingScriptWarnings_.insert(warningKey).second) { - return; - } - editorContext.AddConsoleMessage( - MetaCoreLogLevel::Warning, - "Script", - "脚本类型缺失,已跳过: " + warningKey - ); - } - - std::vector RuntimeEntries_{}; - std::unordered_set MissingScriptWarnings_{}; + std::unique_ptr Runtime_{}; }; class MetaCoreRotatorPlayModeComponentSystem final : public MetaCoreIPlayModeComponentSystem { @@ -12026,6 +11971,7 @@ public: moduleRegistry.RegisterService(std::make_shared()); moduleRegistry.RegisterService(std::make_shared()); moduleRegistry.RegisterService(std::make_shared()); + moduleRegistry.RegisterService(std::make_shared()); const auto assetDatabase = moduleRegistry.ResolveService(); const auto importPipeline = moduleRegistry.ResolveService(); diff --git a/Source/MetaCoreEditor/Private/MetaCoreEditorApp.cpp b/Source/MetaCoreEditor/Private/MetaCoreEditorApp.cpp index 7d78031..ffd60cc 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreEditorApp.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreEditorApp.cpp @@ -2421,6 +2421,11 @@ int MetaCoreEditorApp::Run() { hotReloadService != nullptr) { hotReloadService->Tick(); } + if (const auto projectScriptService = ModuleRegistry_.ResolveService(); + projectScriptService != nullptr) { + const auto playModeService = ModuleRegistry_.ResolveService(); + projectScriptService->Tick(playModeService == nullptr || playModeService->GetState() == MetaCorePlayModeState::Edit); + } const auto currentPlayModeTickTime = std::chrono::steady_clock::now(); const double playModeDeltaSeconds = std::chrono::duration(currentPlayModeTickTime - previousPlayModeTickTime).count(); diff --git a/Source/MetaCoreEditor/Private/MetaCoreEditorServices.cpp b/Source/MetaCoreEditor/Private/MetaCoreEditorServices.cpp index 34210ac..f68cdee 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreEditorServices.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreEditorServices.cpp @@ -49,38 +49,6 @@ void MetaCoreIEditorService::Shutdown(MetaCoreEditorModuleRegistry& moduleRegist (void)moduleRegistry; } -void MetaCoreIScriptBehaviour::OnPlayStart(MetaCoreScriptExecutionContext& context) { - (void)context; -} - -void MetaCoreIScriptBehaviour::FixedUpdate( - MetaCoreScriptExecutionContext& context, - double fixedDeltaSeconds -) { - (void)context; - (void)fixedDeltaSeconds; -} - -void MetaCoreIScriptBehaviour::Update( - MetaCoreScriptExecutionContext& context, - double deltaSeconds -) { - (void)context; - (void)deltaSeconds; -} - -void MetaCoreIScriptBehaviour::LateUpdate( - MetaCoreScriptExecutionContext& context, - double deltaSeconds -) { - (void)context; - (void)deltaSeconds; -} - -void MetaCoreIScriptBehaviour::OnPlayStop(MetaCoreScriptExecutionContext& context) { - (void)context; -} - void MetaCoreIPlayModeComponentSystem::OnPlayStart(MetaCoreEditorContext& editorContext) { (void)editorContext; } diff --git a/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorServices.h b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorServices.h index 91e2d6f..36c04ca 100644 --- a/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorServices.h +++ b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorServices.h @@ -7,6 +7,7 @@ #include "MetaCoreFoundation/MetaCorePackage.h" #include "MetaCoreFoundation/MetaCoreReflection.h" #include "MetaCoreScene/MetaCoreScene.h" +#include "MetaCoreScripting/MetaCoreScripting.h" #include #include @@ -415,47 +416,6 @@ public: [[nodiscard]] virtual bool PasteComponent(std::string_view typeId, MetaCoreGameObject& gameObject) const = 0; }; -enum class MetaCoreScriptFieldType { - Bool = 0, - Float, - Vec3, - String -}; - -struct MetaCoreScriptFieldDefinition { - std::string Name{}; - std::string DisplayName{}; - MetaCoreScriptFieldType Type = MetaCoreScriptFieldType::Float; - bool BoolDefault = false; - float FloatDefault = 0.0F; - glm::vec3 Vec3Default{0.0F, 0.0F, 0.0F}; - std::string StringDefault{}; -}; - -struct MetaCoreScriptExecutionContext { - MetaCoreEditorContext& EditorContext; - MetaCoreGameObject GameObject; - MetaCoreScriptInstanceData& Instance; -}; - -class MetaCoreIScriptBehaviour { -public: - virtual ~MetaCoreIScriptBehaviour() = default; - - virtual void OnPlayStart(MetaCoreScriptExecutionContext& context); - virtual void FixedUpdate(MetaCoreScriptExecutionContext& context, double fixedDeltaSeconds); - virtual void Update(MetaCoreScriptExecutionContext& context, double deltaSeconds); - virtual void LateUpdate(MetaCoreScriptExecutionContext& context, double deltaSeconds); - virtual void OnPlayStop(MetaCoreScriptExecutionContext& context); -}; - -struct MetaCoreScriptDefinition { - std::string TypeId{}; - std::string DisplayName{}; - std::vector Fields{}; - std::function()> Factory{}; -}; - class MetaCoreIScriptRegistry : public MetaCoreIEditorService { public: virtual void RegisterScript(MetaCoreScriptDefinition definition) = 0; @@ -464,6 +424,31 @@ public: [[nodiscard]] virtual std::unique_ptr CreateBehaviour(std::string_view typeId) const = 0; [[nodiscard]] virtual std::string BuildDefaultFieldsJson(std::string_view typeId) const = 0; [[nodiscard]] virtual std::string EnsureFieldsJsonDefaults(std::string_view typeId, std::string_view fieldsJson) const = 0; + [[nodiscard]] virtual MetaCoreScriptRegistry& GetRuntimeRegistry() = 0; + [[nodiscard]] virtual const MetaCoreScriptRegistry& GetRuntimeRegistry() const = 0; +}; + +enum class MetaCoreProjectScriptBuildState { + Idle = 0, + Building, + PendingActivation, + Succeeded, + Failed +}; + +struct MetaCoreProjectScriptStatus { + MetaCoreProjectScriptBuildState State = MetaCoreProjectScriptBuildState::Idle; + std::string Message{}; + std::filesystem::path ModulePath{}; +}; + +class MetaCoreIProjectScriptService : public MetaCoreIEditorService { +public: + [[nodiscard]] virtual bool GenerateProject() = 0; + [[nodiscard]] virtual bool CreateBehaviour(std::string_view behaviourName) = 0; + [[nodiscard]] virtual bool CompileAsync(std::string_view configuration = "Debug") = 0; + virtual void Tick(bool canActivate) = 0; + [[nodiscard]] virtual MetaCoreProjectScriptStatus GetStatus() const = 0; }; enum class MetaCorePlayModeState { diff --git a/Source/MetaCoreFoundation/Private/MetaCoreProject.cpp b/Source/MetaCoreFoundation/Private/MetaCoreProject.cpp index 89716cb..0c25bcb 100644 --- a/Source/MetaCoreFoundation/Private/MetaCoreProject.cpp +++ b/Source/MetaCoreFoundation/Private/MetaCoreProject.cpp @@ -161,7 +161,7 @@ bool MetaCoreCreateProjectSkeleton( } std::error_code errorCode; - const std::array directories = { + const std::array directories = { normalizedRoot / "Assets", normalizedRoot / "Scenes", normalizedRoot / "Runtime", @@ -169,6 +169,7 @@ bool MetaCoreCreateProjectSkeleton( normalizedRoot / "Library", normalizedRoot / "Build", normalizedRoot / "BuildProfiles", + normalizedRoot / "Scripts", normalizedRoot / "Assets" / "Models", normalizedRoot / "Assets" / "Materials", normalizedRoot / "Assets" / "Textures", diff --git a/Source/MetaCoreScripting/Private/MetaCoreScripting.cpp b/Source/MetaCoreScripting/Private/MetaCoreScripting.cpp new file mode 100644 index 0000000..631fcc1 --- /dev/null +++ b/Source/MetaCoreScripting/Private/MetaCoreScripting.cpp @@ -0,0 +1,736 @@ +#include "MetaCoreScripting/MetaCoreScripting.h" +#include "MetaCoreFoundation/MetaCoreHash.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(__linux__) +#include +#endif + +namespace MetaCore { + +namespace { + +using Json = nlohmann::json; + +Json ParseObject(std::string_view text) { + if (text.empty()) return Json::object(); + try { + Json value = Json::parse(text); + return value.is_object() ? value : Json::object(); + } catch (...) { + return Json::object(); + } +} + +Json DefaultJson(const MetaCoreScriptFieldDefinition& field) { + switch (field.Type) { + case MetaCoreScriptFieldType::Bool: return field.BoolDefault; + case MetaCoreScriptFieldType::Int: return field.IntDefault; + case MetaCoreScriptFieldType::Float: return field.FloatDefault; + case MetaCoreScriptFieldType::Vec3: return Json::array({field.Vec3Default.x, field.Vec3Default.y, field.Vec3Default.z}); + case MetaCoreScriptFieldType::String: return field.StringDefault; + case MetaCoreScriptFieldType::AssetRef: return field.AssetDefault.ToString(); + case MetaCoreScriptFieldType::GameObjectRef: return field.GameObjectDefault; + } + return nullptr; +} + +MetaCoreScriptFieldType FromAbiType(std::uint32_t type) { + switch (type) { + case MetaCoreScriptAbiValue_Bool: return MetaCoreScriptFieldType::Bool; + case MetaCoreScriptAbiValue_Int: return MetaCoreScriptFieldType::Int; + case MetaCoreScriptAbiValue_Float: return MetaCoreScriptFieldType::Float; + case MetaCoreScriptAbiValue_Vec3: return MetaCoreScriptFieldType::Vec3; + case MetaCoreScriptAbiValue_String: return MetaCoreScriptFieldType::String; + case MetaCoreScriptAbiValue_AssetRef: return MetaCoreScriptFieldType::AssetRef; + case MetaCoreScriptAbiValue_GameObjectRef: return MetaCoreScriptFieldType::GameObjectRef; + default: return MetaCoreScriptFieldType::String; + } +} + +bool JsonToAbi(const Json& json, MetaCoreScriptValueV1& value) { + value = {}; + if (json.is_boolean()) { + value.Type = MetaCoreScriptAbiValue_Bool; + value.IntValue = json.get() ? 1 : 0; + } else if (json.is_number_integer() || json.is_number_unsigned()) { + value.Type = MetaCoreScriptAbiValue_Int; + value.IntValue = json.get(); + } else if (json.is_number()) { + value.Type = MetaCoreScriptAbiValue_Float; + value.NumberValue = json.get(); + } else if (json.is_array() && json.size() >= 3) { + value.Type = MetaCoreScriptAbiValue_Vec3; + value.Vec3[0] = json[0].get(); + value.Vec3[1] = json[1].get(); + value.Vec3[2] = json[2].get(); + } else if (json.is_string()) { + value.Type = MetaCoreScriptAbiValue_String; + std::snprintf(value.Text, sizeof(value.Text), "%s", json.get_ref().c_str()); + } else { + return false; + } + return true; +} + +Json AbiToJson(const MetaCoreScriptValueV1& value) { + switch (value.Type) { + case MetaCoreScriptAbiValue_Bool: return value.IntValue != 0; + case MetaCoreScriptAbiValue_Int: return value.IntValue; + case MetaCoreScriptAbiValue_Float: return value.NumberValue; + case MetaCoreScriptAbiValue_Vec3: return Json::array({value.Vec3[0], value.Vec3[1], value.Vec3[2]}); + case MetaCoreScriptAbiValue_GameObjectRef: return value.ObjectValue.ObjectId; + case MetaCoreScriptAbiValue_String: + case MetaCoreScriptAbiValue_AssetRef: return std::string(value.Text); + default: return nullptr; + } +} + +std::string ShellQuote(const std::filesystem::path& path) { + std::string text = path.string(); + std::string result = "'"; + for (char c : text) result += c == '\'' ? "'\\''" : std::string(1, c); + return result + "'"; +} + +bool WriteIfMissing(const std::filesystem::path& path, std::string_view text) { + if (std::filesystem::exists(path)) return true; + std::ofstream output(path, std::ios::binary); + output.write(text.data(), static_cast(text.size())); + return output.good(); +} + +std::string SafeIdentifier(std::string_view value) { + std::string result; + for (char c : value) result += std::isalnum(static_cast(c)) ? c : '_'; + return result.empty() ? "MetaCoreProject" : result; +} + +MetaCoreScriptInstanceData* FindInstance(MetaCoreGameObject object, std::uint64_t instanceId) { + if (!object || !object.HasComponent()) return nullptr; + auto& instances = object.GetComponent().Instances; + const auto iterator = std::find_if(instances.begin(), instances.end(), [instanceId](const auto& instance) { + return instance.InstanceId == instanceId; + }); + return iterator == instances.end() ? nullptr : &*iterator; +} + +const MetaCoreScriptHostApiV1& HostApi(); + +struct LoadedLibrary { + void* Handle = nullptr; + MetaCoreScriptModuleApiV1 Api{}; + ~LoadedLibrary() { + if (Api.Shutdown != nullptr) Api.Shutdown(); +#if defined(__linux__) + if (Handle != nullptr) dlclose(Handle); +#endif + } +}; + +class AbiBehaviour final : public MetaCoreIScriptBehaviour { +public: + AbiBehaviour(std::shared_ptr library, MetaCoreScriptBehaviourDescriptorV1 descriptor) + : Library_(std::move(library)), Descriptor_(descriptor), Instance_(Descriptor_.Create != nullptr ? Descriptor_.Create() : nullptr) {} + ~AbiBehaviour() override { + if (Descriptor_.Destroy != nullptr && Instance_ != nullptr) Descriptor_.Destroy(Instance_); + } + + void OnPlayStart(MetaCoreScriptExecutionContext& context) override { Call(context, Descriptor_.OnPlayStart, "OnPlayStart"); } + void FixedUpdate(MetaCoreScriptExecutionContext& context, double delta) override { CallUpdate(context, Descriptor_.FixedUpdate, delta, "FixedUpdate"); } + void Update(MetaCoreScriptExecutionContext& context, double delta) override { CallUpdate(context, Descriptor_.Update, delta, "Update"); } + void LateUpdate(MetaCoreScriptExecutionContext& context, double delta) override { CallUpdate(context, Descriptor_.LateUpdate, delta, "LateUpdate"); } + void OnPlayStop(MetaCoreScriptExecutionContext& context) override { Call(context, Descriptor_.OnPlayStop, "OnPlayStop"); } + +private: + MetaCoreScriptExecutionContextV1 BuildContext(MetaCoreScriptExecutionContext& context) const { + return MetaCoreScriptExecutionContextV1{ + &HostApi(), context.Runtime, + {context.Runtime != nullptr ? context.Runtime->GetWorldGeneration() : 0U, context.GameObject.GetId()}, + context.Instance.InstanceId, context.Instance.ScriptTypeId.c_str(), context.Instance.FieldsJson.c_str(), + {context.Time.Frame, context.Time.DeltaSeconds, context.Time.FixedDeltaSeconds, context.Time.ElapsedSeconds, + context.Time.UnscaledElapsedSeconds, context.Time.TimeScale} + }; + } + void Call(MetaCoreScriptExecutionContext& context, MetaCoreScriptLifecycleCallbackV1 callback, const char* phase) { + if (callback == nullptr) return; + const auto abiContext = BuildContext(context); + const char* error = callback(Instance_, &abiContext); + if (error != nullptr && error[0] != '\0') throw std::runtime_error(std::string(phase) + ": " + error); + } + void CallUpdate(MetaCoreScriptExecutionContext& context, MetaCoreScriptUpdateCallbackV1 callback, double delta, const char* phase) { + if (callback == nullptr) return; + const auto abiContext = BuildContext(context); + const char* error = callback(Instance_, &abiContext, delta); + if (error != nullptr && error[0] != '\0') throw std::runtime_error(std::string(phase) + ": " + error); + } + std::shared_ptr Library_{}; + MetaCoreScriptBehaviourDescriptorV1 Descriptor_{}; + void* Instance_ = nullptr; +}; + +} // namespace + +void MetaCoreIScriptBehaviour::OnPlayStart(MetaCoreScriptExecutionContext&) {} +void MetaCoreIScriptBehaviour::FixedUpdate(MetaCoreScriptExecutionContext&, double) {} +void MetaCoreIScriptBehaviour::Update(MetaCoreScriptExecutionContext&, double) {} +void MetaCoreIScriptBehaviour::LateUpdate(MetaCoreScriptExecutionContext&, double) {} +void MetaCoreIScriptBehaviour::OnPlayStop(MetaCoreScriptExecutionContext&) {} + +bool MetaCoreScriptRegistry::RegisterScript(MetaCoreScriptDefinition definition, std::string* error) { + if (definition.TypeId.empty() || !definition.Factory) { + if (error) *error = "Script TypeId and factory are required"; + return false; + } + std::unordered_set fieldIds; + for (const auto& field : definition.Fields) { + const std::string id(field.StableId()); + if (id.empty() || !fieldIds.insert(id).second) { + if (error) *error = "Script fields require unique non-empty field ids"; + return false; + } + } + const auto existing = std::find_if(Definitions_.begin(), Definitions_.end(), [&](const auto& value) { return value.TypeId == definition.TypeId; }); + if (existing != Definitions_.end()) *existing = std::move(definition); + else Definitions_.push_back(std::move(definition)); + return true; +} + +bool MetaCoreScriptRegistry::ReplaceModule(std::string_view moduleId, std::vector definitions, std::string* error) { + std::unordered_set ids; + for (auto& definition : definitions) { + if (definition.ModuleId.empty()) definition.ModuleId = std::string(moduleId); + if (definition.ModuleId != moduleId || !ids.insert(definition.TypeId).second) { + if (error) *error = "Candidate module contains duplicate types or a mismatched module id"; + return false; + } + MetaCoreScriptRegistry validator; + if (!validator.RegisterScript(definition, error)) return false; + const auto outside = std::find_if(Definitions_.begin(), Definitions_.end(), [&](const auto& current) { + return current.TypeId == definition.TypeId && current.ModuleId != moduleId; + }); + if (outside != Definitions_.end()) { + if (error) *error = "Script TypeId conflicts with another module: " + definition.TypeId; + return false; + } + } + RemoveModule(moduleId); + for (auto& definition : definitions) Definitions_.push_back(std::move(definition)); + return true; +} + +void MetaCoreScriptRegistry::RemoveModule(std::string_view moduleId) { + std::erase_if(Definitions_, [&](const auto& definition) { return definition.ModuleId == moduleId; }); +} +void MetaCoreScriptRegistry::Clear() { Definitions_.clear(); } +const MetaCoreScriptDefinition* MetaCoreScriptRegistry::FindScript(std::string_view typeId) const { + const auto it = std::find_if(Definitions_.begin(), Definitions_.end(), [&](const auto& d) { return d.TypeId == typeId; }); + return it == Definitions_.end() ? nullptr : &*it; +} +std::vector MetaCoreScriptRegistry::GetScriptDefinitions() const { return Definitions_; } +std::unique_ptr MetaCoreScriptRegistry::CreateBehaviour(std::string_view typeId) const { + const auto* definition = FindScript(typeId); + return definition != nullptr && definition->Factory ? definition->Factory() : nullptr; +} +std::string MetaCoreScriptRegistry::BuildDefaultFieldsJson(std::string_view typeId) const { + Json fields = Json::object(); + const auto* definition = FindScript(typeId); + if (definition) for (const auto& field : definition->Fields) fields[std::string(field.StableId())] = DefaultJson(field); + return fields.dump(); +} +std::string MetaCoreScriptRegistry::EnsureFieldsJsonDefaults(std::string_view typeId, std::string_view fieldsJson) const { + Json fields = ParseObject(fieldsJson); + const auto* definition = FindScript(typeId); + if (definition) for (const auto& field : definition->Fields) { + const std::string stable(field.StableId()); + if (!fields.contains(stable)) { + if (!field.Name.empty() && fields.contains(field.Name)) fields[stable] = fields[field.Name]; + else fields[stable] = DefaultJson(field); + } + if (!field.FieldId.empty() && field.Name != stable) fields.erase(field.Name); + } + return fields.dump(); +} + +namespace { +class BuiltinRotator final : public MetaCoreIScriptBehaviour { +public: + void Update(MetaCoreScriptExecutionContext& context, double delta) override { + auto fields = ParseObject(context.Instance.FieldsJson); + if (!fields.value("Enabled", true) || !context.GameObject.HasComponent()) return; + context.GameObject.GetComponent().RotationEulerDegrees.y += + fields.value("DegreesPerSecond", 90.0F) * static_cast(delta); + context.Scene.IncrementRevision(); + } +}; +class BuiltinMover final : public MetaCoreIScriptBehaviour { +public: + void Update(MetaCoreScriptExecutionContext& context, double delta) override { + auto fields = ParseObject(context.Instance.FieldsJson); + if (!fields.value("Enabled", true) || !context.GameObject.HasComponent()) return; + glm::vec3 velocity(0.0F, 1.0F, 0.0F); + if (fields.contains("Velocity") && fields["Velocity"].is_array() && fields["Velocity"].size() >= 3) + velocity = {fields["Velocity"][0].get(), fields["Velocity"][1].get(), fields["Velocity"][2].get()}; + context.GameObject.GetComponent().Position += velocity * static_cast(delta); + context.Scene.IncrementRevision(); + } +}; +} + +void MetaCoreRegisterBuiltinScripts(MetaCoreScriptRegistry& registry) { + MetaCoreScriptDefinition rotator; + rotator.TypeId = "MetaCore.Script.Rotator"; rotator.DisplayName = "旋转脚本"; + rotator.Fields = { + MetaCoreScriptFieldDefinition{"Enabled", "启用", MetaCoreScriptFieldType::Bool, true}, + MetaCoreScriptFieldDefinition{"DegreesPerSecond", "角速度 Y (度/秒)", MetaCoreScriptFieldType::Float, false, 90.0F} + }; + rotator.Factory = [] { return std::make_unique(); }; + (void)registry.RegisterScript(std::move(rotator)); + MetaCoreScriptDefinition mover; + mover.TypeId = "MetaCore.Script.Mover"; mover.DisplayName = "移动脚本"; + mover.Fields = { + MetaCoreScriptFieldDefinition{"Enabled", "启用", MetaCoreScriptFieldType::Bool, true}, + MetaCoreScriptFieldDefinition{"Velocity", "速度", MetaCoreScriptFieldType::Vec3, false, 0.0F, glm::vec3(0.0F, 1.0F, 0.0F)} + }; + mover.Factory = [] { return std::make_unique(); }; + (void)registry.RegisterScript(std::move(mover)); +} + +std::optional MetaCoreScriptModuleLoader::LoadCandidate(const std::filesystem::path& path, std::string* error) const { +#if !defined(__linux__) + if (error) *error = "Native project scripts are currently supported only on Linux"; + return std::nullopt; +#else + if (!std::filesystem::is_regular_file(path)) { + if (error) *error = "Script module does not exist: " + path.string(); + return std::nullopt; + } + std::string manifestModuleId; + std::string manifestBuildId; + std::filesystem::path manifestPath = path; + manifestPath += ".mcscriptmodule.json"; + if (std::filesystem::is_regular_file(manifestPath)) { + try { + std::ifstream manifestInput(manifestPath); + Json manifest = Json::parse(manifestInput); + const auto hash = MetaCoreSha256File(path); + manifestModuleId = manifest.value("module_id", ""); + manifestBuildId = manifest.value("build_id", ""); + if (manifest.value("format_version", 0U) != 1U || + manifest.value("abi_major", 0U) != METACORE_SCRIPT_ABI_MAJOR || + manifest.value("abi_minor", 0U) > METACORE_SCRIPT_ABI_MINOR || + manifest.value("module_file", "") != path.filename().string() || + !hash.has_value() || manifest.value("sha256", "") != *hash) { + if (error) *error = "Script module manifest or SHA-256 validation failed"; + return std::nullopt; + } + } catch (...) { + if (error) *error = "Script module manifest is unreadable"; + return std::nullopt; + } + } + auto library = std::make_shared(); + library->Handle = dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL); + if (!library->Handle) { + if (error) *error = dlerror(); + return std::nullopt; + } + auto load = reinterpret_cast(dlsym(library->Handle, "MetaCoreLoadScriptModuleV1")); + if (!load) { + if (error) *error = "Module does not export MetaCoreLoadScriptModuleV1"; + return std::nullopt; + } + try { + if (!load(&HostApi(), &library->Api)) { + if (error) *error = "Module initialization rejected the host"; + return std::nullopt; + } + } catch (...) { + if (error) *error = "Module initialization crossed the ABI with an exception"; + return std::nullopt; + } + const auto& api = library->Api; + if (api.AbiMajor != METACORE_SCRIPT_ABI_MAJOR || api.AbiMinor > METACORE_SCRIPT_ABI_MINOR || + (api.RequiredCapabilities & ~METACORE_SCRIPT_HOST_CAPABILITIES) != 0 || api.ModuleId == nullptr || api.ModuleId[0] == '\0') { + if (error) *error = "Module ABI, capabilities, or module id are incompatible"; + return std::nullopt; + } + if (api.BehaviourCount > 0 && api.Behaviours == nullptr) { + if (error) *error = "Module behaviour table is null"; + return std::nullopt; + } + MetaCoreLoadedScriptModule result; + result.ModuleId = api.ModuleId; + if (!manifestModuleId.empty() && manifestModuleId != result.ModuleId) { + if (error) *error = "Loaded module id does not match its validated manifest"; + return std::nullopt; + } + result.BuildId = api.BuildId != nullptr ? api.BuildId : ""; + result.Path = path; + result.Lifetime = library; + std::unordered_set typeIds; + for (std::size_t i = 0; i < api.BehaviourCount; ++i) { + const auto descriptor = api.Behaviours[i]; + if (descriptor.TypeId == nullptr || descriptor.TypeId[0] == '\0' || descriptor.Create == nullptr || descriptor.Destroy == nullptr || + (descriptor.FieldCount > 0 && descriptor.Fields == nullptr) || !typeIds.insert(descriptor.TypeId).second) { + if (error) *error = "Module contains an invalid or duplicate behaviour descriptor"; + return std::nullopt; + } + if (!manifestBuildId.empty() && manifestBuildId != result.BuildId) { + if (error) *error = "Loaded module build id does not match its validated manifest"; + return std::nullopt; + } + MetaCoreScriptDefinition definition; + definition.TypeId = descriptor.TypeId; + definition.DisplayName = descriptor.DisplayName != nullptr ? descriptor.DisplayName : descriptor.TypeId; + definition.ModuleId = result.ModuleId; + definition.SchemaVersion = descriptor.SchemaVersion; + std::unordered_set fieldIds; + for (std::size_t fieldIndex = 0; fieldIndex < descriptor.FieldCount; ++fieldIndex) { + const auto& source = descriptor.Fields[fieldIndex]; + if (source.FieldId == nullptr || source.FieldId[0] == '\0' || !fieldIds.insert(source.FieldId).second) { + if (error) *error = "Module contains an invalid or duplicate field id"; + return std::nullopt; + } + MetaCoreScriptFieldDefinition field; + field.Name = source.FieldId; + field.FieldId = source.FieldId; + field.DisplayName = source.DisplayName != nullptr ? source.DisplayName : source.FieldId; + field.Type = FromAbiType(source.Type); + Json defaultValue; + try { defaultValue = source.DefaultJson != nullptr ? Json::parse(source.DefaultJson) : Json{}; } + catch (...) { if (error) *error = "Field default JSON is invalid"; return std::nullopt; } + if (field.Type == MetaCoreScriptFieldType::Bool && defaultValue.is_boolean()) field.BoolDefault = defaultValue.get(); + else if (field.Type == MetaCoreScriptFieldType::Int && defaultValue.is_number_integer()) field.IntDefault = defaultValue.get(); + else if (field.Type == MetaCoreScriptFieldType::Float && defaultValue.is_number()) field.FloatDefault = defaultValue.get(); + else if (field.Type == MetaCoreScriptFieldType::Vec3 && defaultValue.is_array() && defaultValue.size() >= 3) field.Vec3Default = {defaultValue[0].get(), defaultValue[1].get(), defaultValue[2].get()}; + else if ((field.Type == MetaCoreScriptFieldType::String || field.Type == MetaCoreScriptFieldType::AssetRef) && defaultValue.is_string()) field.StringDefault = defaultValue.get(); + else if (field.Type == MetaCoreScriptFieldType::GameObjectRef && defaultValue.is_number_unsigned()) field.GameObjectDefault = defaultValue.get(); + definition.Fields.push_back(std::move(field)); + } + definition.Factory = [library, descriptor]() { return std::make_unique(library, descriptor); }; + result.Definitions.push_back(std::move(definition)); + } + return result; +#endif +} + +struct MetaCoreScriptRuntime::Impl { + struct Entry { MetaCoreId ObjectId = 0; std::uint64_t InstanceId = 0; std::string TypeId; std::unique_ptr Behaviour; bool Started = false; bool Faulted = false; }; + struct Event { std::string Topic; std::vector Fields; }; + struct Subscription { std::uint64_t Id; MetaCoreId ObjectId; std::uint64_t InstanceId; std::string Topic; EventHandler Handler; }; + struct Timer { std::uint64_t Id; MetaCoreId ObjectId; std::uint64_t InstanceId; double Remaining; double Interval; bool Unscaled; std::function Callback; }; + + MetaCoreScriptRuntime& Owner; + MetaCoreScene& Scene; + MetaCoreScriptRegistry& Registry; + MetaCoreScriptLogHandler Logger; + std::vector Entries; + std::vector Events; + std::vector Subscriptions; + std::vector Timers; + std::uint64_t NextSubscription = 1; + std::uint64_t NextTimer = 1; + bool Running = false; + + Impl(MetaCoreScriptRuntime& owner, MetaCoreScene& scene, MetaCoreScriptRegistry& registry, MetaCoreScriptLogHandler logger) + : Owner(owner), Scene(scene), Registry(registry), Logger(std::move(logger)) {} + + void Log(std::uint32_t level, std::string_view category, std::string_view text) const { if (Logger) Logger(level, category, text); } + Entry* FindEntry(MetaCoreId object, std::uint64_t instance) { + auto it = std::find_if(Entries.begin(), Entries.end(), [&](const auto& e) { return e.ObjectId == object && e.InstanceId == instance; }); + return it == Entries.end() ? nullptr : &*it; + } + MetaCoreScriptExecutionContext Context(MetaCoreGameObject object, MetaCoreScriptInstanceData& instance) { + return {Scene, object, instance, &Owner, Owner.Time_}; + } + void Fault(Entry& entry, std::string_view phase, std::string_view message) { + entry.Faulted = true; + std::ostringstream text; + text << "module/script callback fault type=\"" << entry.TypeId << "\" object=" << entry.ObjectId + << " instance=" << entry.InstanceId << " phase=" << phase << " error=" << message; + Log(2, "Script", text.str()); + CleanupOwner(entry.ObjectId, entry.InstanceId); + } + void CleanupOwner(MetaCoreId object, std::uint64_t instance) { + std::erase_if(Subscriptions, [&](const auto& s) { return s.ObjectId == object && s.InstanceId == instance; }); + std::erase_if(Timers, [&](const auto& t) { return t.ObjectId == object && t.InstanceId == instance; }); + } + Entry* Ensure(MetaCoreGameObject object, MetaCoreScriptInstanceData& instance) { + if (instance.InstanceId == 0) instance.InstanceId = MetaCoreIdGenerator::Generate(); + if (auto* current = FindEntry(object.GetId(), instance.InstanceId)) return current; + const auto* definition = Registry.FindScript(instance.ScriptTypeId); + if (!definition) { Log(1, "Script", "Missing script type: " + instance.ScriptTypeId); return nullptr; } + instance.FieldsJson = Registry.EnsureFieldsJsonDefaults(instance.ScriptTypeId, instance.FieldsJson); + Entry entry{object.GetId(), instance.InstanceId, instance.ScriptTypeId, Registry.CreateBehaviour(instance.ScriptTypeId)}; + if (!entry.Behaviour) return nullptr; + Entries.push_back(std::move(entry)); + auto& created = Entries.back(); + try { auto context = Context(object, instance); created.Behaviour->OnPlayStart(context); created.Started = true; } + catch (const std::exception& e) { Fault(created, "OnPlayStart", e.what()); } + catch (...) { Fault(created, "OnPlayStart", "unknown exception"); } + return &created; + } + void CleanupStale() { + for (auto it = Entries.begin(); it != Entries.end();) { + auto object = Scene.FindGameObject(it->ObjectId); + auto* instance = FindInstance(object, it->InstanceId); + if (!object || !instance || !instance->Enabled || instance->ScriptTypeId != it->TypeId) { + StopEntry(*it, object, instance); + CleanupOwner(it->ObjectId, it->InstanceId); + it = Entries.erase(it); + } else ++it; + } + } + void StopEntry(Entry& entry, MetaCoreGameObject object, MetaCoreScriptInstanceData* instance) { + if (!entry.Started || !object || !instance) return; + try { auto context = Context(object, *instance); entry.Behaviour->OnPlayStop(context); } + catch (const std::exception& e) { Log(2, "Script", std::string("OnPlayStop fault: ") + e.what()); } + catch (...) { Log(2, "Script", "OnPlayStop fault: unknown exception"); } + entry.Started = false; + } + enum class Phase { Fixed, Update, Late }; + void Dispatch(Phase phase, double delta) { + CleanupStale(); + for (auto object : Scene.GetGameObjects()) { + if (!object.HasComponent()) continue; + for (auto& instance : object.GetComponent().Instances) { + if (!instance.Enabled) continue; + auto* entry = Ensure(object, instance); + if (!entry || entry->Faulted) continue; + try { + auto context = Context(object, instance); + if (phase == Phase::Fixed) entry->Behaviour->FixedUpdate(context, delta); + else if (phase == Phase::Update) entry->Behaviour->Update(context, delta); + else entry->Behaviour->LateUpdate(context, delta); + } catch (const std::exception& e) { Fault(*entry, phase == Phase::Fixed ? "FixedUpdate" : phase == Phase::Update ? "Update" : "LateUpdate", e.what()); } + catch (...) { Fault(*entry, "Update", "unknown exception"); } + } + } + } + void DispatchEvents() { + auto current = std::move(Events); + Events.clear(); + for (const auto& event : current) { + const auto subscriptions = Subscriptions; + for (const auto& subscription : subscriptions) { + if (subscription.Topic == event.Topic && FindInstance(Scene.FindGameObject(subscription.ObjectId), subscription.InstanceId)) { + try { subscription.Handler(event.Topic, event.Fields); } + catch (...) { Log(2, "Script.Event", "Event handler threw an exception"); } + } + } + } + } + void TickTimers(double scaled, double unscaled) { + std::erase_if(Timers, [&](const auto& timer) { + return !FindInstance(Scene.FindGameObject(timer.ObjectId), timer.InstanceId); + }); + std::vector fire; + for (auto& timer : Timers) { timer.Remaining -= timer.Unscaled ? unscaled : scaled; if (timer.Remaining <= 0.0) fire.push_back(timer.Id); } + for (auto id : fire) { + auto it = std::find_if(Timers.begin(), Timers.end(), [id](const auto& t) { return t.Id == id; }); + if (it == Timers.end()) continue; + auto callback = it->Callback; + const double interval = it->Interval; + if (interval > 0.0) it->Remaining += interval; + else Timers.erase(it); + try { callback(); } catch (...) { Log(2, "Script.Timer", "Timer callback threw an exception"); } + } + } +}; + +MetaCoreScriptRuntime::MetaCoreScriptRuntime(MetaCoreScene& scene, MetaCoreScriptRegistry& registry, MetaCoreScriptLogHandler logger) + : Impl_(std::make_unique(*this, scene, registry, std::move(logger))) {} +MetaCoreScriptRuntime::~MetaCoreScriptRuntime() { Stop(); } +void MetaCoreScriptRuntime::Start() { if (Impl_->Running) return; Impl_->Running = true; ++WorldGeneration_; for (auto o : Impl_->Scene.GetGameObjects()) if (o.HasComponent()) for (auto& i : o.GetComponent().Instances) if (i.Enabled) Impl_->Ensure(o, i); } +void MetaCoreScriptRuntime::FixedUpdate(double delta) { if (!Impl_->Running) return; Time_.FixedDeltaSeconds = std::max(0.0, delta); Impl_->Dispatch(Impl::Phase::Fixed, Time_.FixedDeltaSeconds); } +void MetaCoreScriptRuntime::Update(double delta) { + if (!Impl_->Running) return; + const double unscaled = std::max(0.0, delta); const double scaled = unscaled * Time_.TimeScale; + ++Time_.Frame; Time_.DeltaSeconds = scaled; Time_.ElapsedSeconds += scaled; Time_.UnscaledElapsedSeconds += unscaled; + Impl_->TickTimers(scaled, unscaled); Impl_->DispatchEvents(); Impl_->Dispatch(Impl::Phase::Update, scaled); +} +void MetaCoreScriptRuntime::LateUpdate(double delta) { if (Impl_->Running) Impl_->Dispatch(Impl::Phase::Late, std::max(0.0, delta) * Time_.TimeScale); } +void MetaCoreScriptRuntime::Stop() { + if (!Impl_ || !Impl_->Running) return; + for (auto& entry : Impl_->Entries) { auto object = Impl_->Scene.FindGameObject(entry.ObjectId); Impl_->StopEntry(entry, object, FindInstance(object, entry.InstanceId)); } + Impl_->Entries.clear(); Impl_->Events.clear(); Impl_->Subscriptions.clear(); Impl_->Timers.clear(); Impl_->Running = false; Time_ = {}; ++WorldGeneration_; +} +void MetaCoreScriptRuntime::SetTimeScale(double scale) { Time_.TimeScale = std::max(0.0, scale); } +std::size_t MetaCoreScriptRuntime::GetFaultedInstanceCount() const { return std::count_if(Impl_->Entries.begin(), Impl_->Entries.end(), [](const auto& e) { return e.Faulted; }); } +std::uint64_t MetaCoreScriptRuntime::Subscribe(MetaCoreId object, std::uint64_t instance, std::string topic, EventHandler handler) { const auto id = Impl_->NextSubscription++; Impl_->Subscriptions.push_back({id, object, instance, std::move(topic), std::move(handler)}); return id; } +void MetaCoreScriptRuntime::Unsubscribe(std::uint64_t id) { std::erase_if(Impl_->Subscriptions, [id](const auto& s) { return s.Id == id; }); } +void MetaCoreScriptRuntime::Publish(std::string topic, std::vector fields) { Impl_->Events.push_back({std::move(topic), std::move(fields)}); } +std::uint64_t MetaCoreScriptRuntime::ScheduleTimer(MetaCoreId object, std::uint64_t instance, double delay, double interval, bool unscaled, std::function callback) { const auto id = Impl_->NextTimer++; Impl_->Timers.push_back({id, object, instance, std::max(0.0, delay), std::max(0.0, interval), unscaled, std::move(callback)}); return id; } +void MetaCoreScriptRuntime::CancelTimer(std::uint64_t id) { std::erase_if(Impl_->Timers, [id](const auto& t) { return t.Id == id; }); } +bool MetaCoreScriptRuntime::IsHandleValid(MetaCoreScriptObjectHandleV1 handle) const { return handle.WorldGeneration == WorldGeneration_ && handle.ObjectId != 0 && static_cast(Impl_->Scene.FindGameObject(handle.ObjectId)); } +bool MetaCoreScriptRuntime::HasComponent(MetaCoreScriptObjectHandleV1 handle, std::string_view component) const { + auto object = IsHandleValid(handle) ? Impl_->Scene.FindGameObject(handle.ObjectId) : MetaCoreGameObject{}; + if (!object) return false; + if (component == "Transform" || component == "Name") return true; + if (component == "Script") return object.HasComponent(); + if (component == "Camera") return object.HasComponent(); + if (component == "Light") return object.HasComponent(); + if (component == "MeshRenderer") return object.HasComponent(); + if (component == "Animator") return object.HasComponent(); + return false; +} +bool MetaCoreScriptRuntime::GetProperty(MetaCoreScriptObjectHandleV1 handle, std::string_view component, std::string_view property, MetaCoreScriptValueV1& value) const { + if (!IsHandleValid(handle)) return false; auto object = Impl_->Scene.FindGameObject(handle.ObjectId); value = {}; + if (component == "Name" && property == "Value") { value.Type = MetaCoreScriptAbiValue_String; std::snprintf(value.Text, sizeof(value.Text), "%s", object.GetName().c_str()); return true; } + if (component != "Transform") return false; const auto& transform = object.GetComponent(); const glm::vec3* source = nullptr; + if (property == "Position") source = &transform.Position; else if (property == "Rotation") source = &transform.RotationEulerDegrees; else if (property == "Scale") source = &transform.Scale; else return false; + value.Type = MetaCoreScriptAbiValue_Vec3; value.Vec3[0] = source->x; value.Vec3[1] = source->y; value.Vec3[2] = source->z; return true; +} +bool MetaCoreScriptRuntime::SetProperty(MetaCoreScriptObjectHandleV1 handle, std::string_view component, std::string_view property, const MetaCoreScriptValueV1& value) { + if (!IsHandleValid(handle)) return false; auto object = Impl_->Scene.FindGameObject(handle.ObjectId); + if (component == "Name" && property == "Value" && value.Type == MetaCoreScriptAbiValue_String) { object.GetName() = value.Text; Impl_->Scene.IncrementRevision(); return true; } + if (component != "Transform" || value.Type != MetaCoreScriptAbiValue_Vec3) return false; auto& transform = object.GetComponent(); glm::vec3* target = nullptr; + if (property == "Position") target = &transform.Position; else if (property == "Rotation") target = &transform.RotationEulerDegrees; else if (property == "Scale") target = &transform.Scale; else return false; + *target = {static_cast(value.Vec3[0]), static_cast(value.Vec3[1]), static_cast(value.Vec3[2])}; Impl_->Scene.IncrementRevision(); return true; +} +bool MetaCoreScriptRuntime::GetField(MetaCoreScriptObjectHandleV1 handle, std::uint64_t instance, std::string_view field, MetaCoreScriptValueV1& value) const { + if (!IsHandleValid(handle)) return false; + auto* data = FindInstance(Impl_->Scene.FindGameObject(handle.ObjectId), instance); + if (!data) return false; + auto json = ParseObject(data->FieldsJson); + if (!json.contains(std::string(field)) || !JsonToAbi(json[std::string(field)], value)) return false; + if (const auto* definition = Impl_->Registry.FindScript(data->ScriptTypeId); definition != nullptr) { + const auto fieldDefinition = std::find_if(definition->Fields.begin(), definition->Fields.end(), [&](const auto& candidate) { return candidate.StableId() == field; }); + if (fieldDefinition != definition->Fields.end()) { + if (fieldDefinition->Type == MetaCoreScriptFieldType::AssetRef) value.Type = MetaCoreScriptAbiValue_AssetRef; + if (fieldDefinition->Type == MetaCoreScriptFieldType::GameObjectRef) { + value.Type = MetaCoreScriptAbiValue_GameObjectRef; + value.ObjectValue = {WorldGeneration_, static_cast(json[std::string(field)].get())}; + } + } + } + return true; +} +bool MetaCoreScriptRuntime::SetField(MetaCoreScriptObjectHandleV1 handle, std::uint64_t instance, std::string_view field, const MetaCoreScriptValueV1& value) { if (!IsHandleValid(handle)) return false; auto* data = FindInstance(Impl_->Scene.FindGameObject(handle.ObjectId), instance); if (!data) return false; auto json = ParseObject(data->FieldsJson); json[std::string(field)] = AbiToJson(value); data->FieldsJson = json.dump(); Impl_->Scene.IncrementRevision(); return true; } +void MetaCoreScriptRuntime::Log(std::uint32_t level, std::string_view category, std::string_view message) const { Impl_->Log(level, category, message); } + +namespace { +bool HValid(void* c, MetaCoreScriptObjectHandleV1 h) noexcept { try { return c && static_cast(c)->IsHandleValid(h); } catch (...) { return false; } } +bool HHas(void* c, MetaCoreScriptObjectHandleV1 h, const char* t) noexcept { try { return c && t && static_cast(c)->HasComponent(h, t); } catch (...) { return false; } } +bool HGet(void* c, MetaCoreScriptObjectHandleV1 h, const char* t, const char* p, MetaCoreScriptValueV1* v) noexcept { try { return c && t && p && v && static_cast(c)->GetProperty(h, t, p, *v); } catch (...) { return false; } } +bool HSet(void* c, MetaCoreScriptObjectHandleV1 h, const char* t, const char* p, const MetaCoreScriptValueV1* v) noexcept { try { return c && t && p && v && static_cast(c)->SetProperty(h, t, p, *v); } catch (...) { return false; } } +bool HGetField(void* c, MetaCoreScriptObjectHandleV1 h, std::uint64_t i, const char* f, MetaCoreScriptValueV1* v) noexcept { try { return c && f && v && static_cast(c)->GetField(h, i, f, *v); } catch (...) { return false; } } +bool HSetField(void* c, MetaCoreScriptObjectHandleV1 h, std::uint64_t i, const char* f, const MetaCoreScriptValueV1* v) noexcept { try { return c && f && v && static_cast(c)->SetField(h, i, f, *v); } catch (...) { return false; } } +void HLog(void* c, std::uint32_t l, const char* k, const char* m) noexcept { try { if (c) static_cast(c)->Log(l, k ? k : "Script", m ? m : ""); } catch (...) {} } +bool HPublish(void* c, const char* topic, const MetaCoreScriptValueV1* values, std::size_t count) noexcept { try { if (!c || !topic) return false; std::vector copied(values, values + count); static_cast(c)->Publish(topic, std::move(copied)); return true; } catch (...) { return false; } } +std::uint64_t HSubscribe(void* c, MetaCoreScriptObjectHandleV1 h, std::uint64_t instance, const char* topic, MetaCoreScriptEventCallbackV1 cb, void* user) noexcept { try { if (!c || !topic || !cb) return 0; auto* runtime = static_cast(c); return runtime->Subscribe(h.ObjectId, instance, topic, [runtime, h, instance, cb, user](std::string_view name, const std::vector& fields) { MetaCoreScriptExecutionContextV1 context{&HostApi(), runtime, h, instance, "", "{}", {}}; if (const char* e = cb(user, &context, std::string(name).c_str(), fields.data(), fields.size()); e) runtime->Log(2, "Script.Event", e); }); } catch (...) { return 0; } } +void HUnsubscribe(void* c, std::uint64_t id) noexcept { try { if (c) static_cast(c)->Unsubscribe(id); } catch (...) {} } +std::uint64_t HSchedule(void* c, MetaCoreScriptObjectHandleV1 h, std::uint64_t instance, double delay, double interval, bool unscaled, MetaCoreScriptTimerCallbackV1 cb, void* user) noexcept { try { if (!c || !cb) return 0; auto* runtime = static_cast(c); return runtime->ScheduleTimer(h.ObjectId, instance, delay, interval, unscaled, [runtime, h, instance, cb, user] { MetaCoreScriptExecutionContextV1 context{&HostApi(), runtime, h, instance, "", "{}", {}}; if (const char* e = cb(user, &context); e) runtime->Log(2, "Script.Timer", e); }); } catch (...) { return 0; } } +void HCancel(void* c, std::uint64_t id) noexcept { try { if (c) static_cast(c)->CancelTimer(id); } catch (...) {} } +const MetaCoreScriptHostApiV1& HostApi() { static const MetaCoreScriptHostApiV1 api{METACORE_SCRIPT_ABI_MAJOR, METACORE_SCRIPT_ABI_MINOR, METACORE_SCRIPT_HOST_CAPABILITIES, HValid, HHas, HGet, HSet, HGetField, HSetField, HLog, HPublish, HSubscribe, HUnsubscribe, HSchedule, HCancel}; return api; } +} // namespace + +bool MetaCoreGenerateScriptProject(const std::filesystem::path& root, std::string_view projectName, const std::filesystem::path& sdkRoot, std::string* error) { + const auto scripts = root / "Scripts"; std::error_code ec; + std::filesystem::create_directories(scripts / "Source", ec); std::filesystem::create_directories(scripts / "SDK" / "MetaCoreScripting", ec); std::filesystem::create_directories(root / ".vscode", ec); + const auto sourceHeader = sdkRoot / "Source" / "MetaCoreScripting" / "Public" / "MetaCoreScripting" / "MetaCoreScriptAbi.h"; + if (ec || !std::filesystem::is_regular_file(sourceHeader)) { if (error) *error = "MetaCore scripting SDK header was not found at " + sourceHeader.string(); return false; } + std::filesystem::copy_file(sourceHeader, scripts / "SDK" / "MetaCoreScripting" / "MetaCoreScriptAbi.h", std::filesystem::copy_options::overwrite_existing, ec); + if (ec) { if (error) *error = ec.message(); return false; } + const std::string id = SafeIdentifier(projectName); + const std::string cmake = "cmake_minimum_required(VERSION 3.20)\nproject(" + id + "Scripts LANGUAGES CXX)\nif(NOT CMAKE_SYSTEM_NAME STREQUAL \"Linux\" OR NOT CMAKE_SIZEOF_VOID_P EQUAL 8)\n message(FATAL_ERROR \"MetaCore script v1 requires Linux x86_64\")\nendif()\nif(NOT CMAKE_CXX_COMPILER_ID MATCHES \"Clang\")\n message(FATAL_ERROR \"MetaCore script v1 requires Clang and libc++\")\nendif()\nset(CMAKE_CXX_STANDARD 20)\nset(CMAKE_POSITION_INDEPENDENT_CODE ON)\nfile(GLOB SCRIPT_SOURCES CONFIGURE_DEPENDS Source/*.cpp)\nadd_library(" + id + "Scripts SHARED ${SCRIPT_SOURCES})\ntarget_include_directories(" + id + "Scripts PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/SDK)\ntarget_compile_definitions(" + id + "Scripts PRIVATE METACORE_SCRIPT_MODULE_ID=\\\"" + id + "\\\" METACORE_SCRIPT_BUILD_ID=\\\"${METACORE_SCRIPT_BUILD_ID}\\\")\nset_target_properties(" + id + "Scripts PROPERTIES PREFIX \"\" OUTPUT_NAME \"" + id + "Scripts_${METACORE_SCRIPT_BUILD_ID}\" LIBRARY_OUTPUT_DIRECTORY \"${METACORE_SCRIPT_OUTPUT_DIR}\")\n"; + const std::string sdk = "#pragma once\n#include \n#include \n#include \n#include \nnamespace MetaCoreScriptSdk { inline std::vector& Registry(){ static std::vector value; return value; } struct Registrar { explicit Registrar(const MetaCoreScriptBehaviourDescriptorV1* value){ Registry().push_back(value); } }; template const char* Guard(F&& function) noexcept { try { function(); return nullptr; } catch(const std::exception& e) { static thread_local std::string error; error=e.what(); return error.c_str(); } catch(...) { return \"unknown C++ exception\"; } } }\n#define MC_REGISTER_SCRIPT(Name, Descriptor) static MetaCoreScriptSdk::Registrar Name##Registrar(&(Descriptor))\n"; + const std::string module = "#include \n#include \nextern \"C\" METACORE_SCRIPT_EXPORT bool MetaCoreLoadScriptModuleV1(const MetaCoreScriptHostApiV1* host, MetaCoreScriptModuleApiV1* out) noexcept { if(!host||!out||host->AbiMajor!=METACORE_SCRIPT_ABI_MAJOR) return false; static std::vector descriptors; if(descriptors.empty()) for(const auto* descriptor:MetaCoreScriptSdk::Registry()) descriptors.push_back(*descriptor); *out={METACORE_SCRIPT_ABI_MAJOR,METACORE_SCRIPT_ABI_MINOR,METACORE_SCRIPT_CAP_PROPERTIES|METACORE_SCRIPT_CAP_EVENTS|METACORE_SCRIPT_CAP_TIMERS,METACORE_SCRIPT_MODULE_ID,METACORE_SCRIPT_BUILD_ID,descriptors.size(),descriptors.data(),nullptr}; return true; }\n"; + const std::string launch = "{\n \"version\": \"0.2.0\",\n \"configurations\": [\n {\"name\": \"Attach MetaCore Editor (LLDB)\", \"type\": \"lldb\", \"request\": \"attach\", \"pid\": \"${command:pickMyProcess}\"},\n {\"name\": \"Attach MetaCore Player (LLDB)\", \"type\": \"lldb\", \"request\": \"attach\", \"pid\": \"${command:pickMyProcess}\"}\n ]\n}\n"; + if (!WriteIfMissing(scripts / "CMakeLists.txt", cmake) || + !WriteIfMissing(scripts / "SDK" / "MetaCoreScripting" / "MetaCoreScriptSdk.h", sdk) || + !WriteIfMissing(scripts / "Source" / "Module.cpp", module) || + !WriteIfMissing(root / ".vscode" / "launch.json", launch)) { + if (error) *error = "Failed to write script project files"; + return false; + } + if (std::filesystem::exists(scripts / "Source" / "Mover.cpp")) { + return true; + } + return MetaCoreCreateScriptBehaviour(root, projectName, "Mover", error); +} + +bool MetaCoreCreateScriptBehaviour(const std::filesystem::path& root, std::string_view projectName, std::string_view behaviourName, std::string* error) { + const std::string moduleId = SafeIdentifier(projectName); + const std::string name = SafeIdentifier(behaviourName); + if (name.empty() || name != behaviourName) { + if (error) *error = "Behaviour name must contain only ASCII letters, digits, or underscores"; + return false; + } + const auto path = root / "Scripts" / "Source" / (name + ".cpp"); + if (std::filesystem::exists(path)) { + if (error) *error = "Behaviour source already exists: " + path.string(); + return false; + } + const std::string source = "#include \n#include \nstruct " + name + " {};\nstatic void* Create" + name + "() noexcept { return new (std::nothrow) " + name + "; }\nstatic void Destroy" + name + "(void* value) noexcept { delete static_cast<" + name + "*>(value); }\nstatic const char* Update" + name + "(void*, const MetaCoreScriptExecutionContextV1* context, double delta) noexcept { return MetaCoreScriptSdk::Guard([&] { MetaCoreScriptValueV1 speed{}; if(!context->Host->GetField(context->HostContext,context->Object,context->InstanceId,\"speed\",&speed)) return; MetaCoreScriptValueV1 position{}; if(!context->Host->GetProperty(context->HostContext,context->Object,\"Transform\",\"Position\",&position)) return; position.Vec3[0]+=speed.NumberValue*delta; context->Host->SetProperty(context->HostContext,context->Object,\"Transform\",\"Position\",&position); }); }\nstatic const MetaCoreScriptFieldDescriptorV1 " + name + "Fields[]={{\"speed\",\"Speed\",MetaCoreScriptAbiValue_Float,\"1.0\"}};\nstatic const MetaCoreScriptBehaviourDescriptorV1 " + name + "Descriptor={\"" + moduleId + "." + name + "\",\"" + name + "\",1,1," + name + "Fields,Create" + name + ",Destroy" + name + ",nullptr,nullptr,Update" + name + ",nullptr,nullptr};\nMC_REGISTER_SCRIPT(" + name + ", " + name + "Descriptor);\n"; + if (!WriteIfMissing(path, source)) { + if (error) *error = "Failed to create behaviour source: " + path.string(); + return false; + } + return true; +} + +MetaCoreScriptProjectBuildResult MetaCoreBuildScriptProject(const std::filesystem::path& root, std::string_view configuration, const std::filesystem::path&) { + MetaCoreScriptProjectBuildResult result; const auto now = std::chrono::system_clock::now().time_since_epoch(); result.BuildId = std::to_string(std::chrono::duration_cast(now).count()); + result.BuildDirectory = root / "Library" / "ScriptBuild" / std::string(configuration); + const auto output = root / "Library" / "ScriptModules" / "Linux-x86_64" / std::string(configuration) / result.BuildId; + std::error_code ec; + std::filesystem::remove(result.BuildDirectory / "CMakeCache.txt", ec); + ec.clear(); + std::filesystem::remove_all(result.BuildDirectory / "CMakeFiles", ec); + ec.clear(); + std::filesystem::create_directories(result.BuildDirectory, ec); std::filesystem::create_directories(output, ec); + const auto log = result.BuildDirectory / ("build-" + result.BuildId + ".log"); + const std::string configure = "cmake -S " + ShellQuote(root / "Scripts") + " -B " + ShellQuote(result.BuildDirectory) + " -DCMAKE_CXX_COMPILER=clang++-15 -DCMAKE_CXX_FLAGS=-stdlib=libc++ -DCMAKE_SHARED_LINKER_FLAGS=-stdlib=libc++ -DCMAKE_BUILD_TYPE=" + std::string(configuration) + " -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DMETACORE_SCRIPT_BUILD_ID=" + result.BuildId + " -DMETACORE_SCRIPT_OUTPUT_DIR=" + ShellQuote(output) + " > " + ShellQuote(log) + " 2>&1"; + int code = std::system(configure.c_str()); + if (code == 0) { const std::string build = "cmake --build " + ShellQuote(result.BuildDirectory) + " --config " + std::string(configuration) + " >> " + ShellQuote(log) + " 2>&1"; code = std::system(build.c_str()); } + std::ifstream input(log); std::ostringstream text; text << input.rdbuf(); result.Output = text.str(); + if (code == 0) { for (const auto& entry : std::filesystem::directory_iterator(output, ec)) if (entry.path().extension() == ".so") { result.ModulePath = entry.path(); break; } } + result.Success = code == 0 && !result.ModulePath.empty(); + if (result.Success) { + std::filesystem::copy_file(result.BuildDirectory / "compile_commands.json", root / "compile_commands.json", std::filesystem::copy_options::overwrite_existing, ec); + std::string loadError; + MetaCoreScriptModuleLoader loader; + const auto loaded = loader.LoadCandidate(result.ModulePath, &loadError); + const auto hash = MetaCoreSha256File(result.ModulePath); + if (!loaded.has_value() || !hash.has_value()) { + result.Success = false; + result.Output += "\nPost-build module validation failed: " + loadError; + } else { + result.ManifestPath = result.ModulePath; + result.ManifestPath += ".mcscriptmodule.json"; + std::ofstream manifest(result.ManifestPath, std::ios::trunc); + manifest << Json{{"format_version", 1}, {"abi_major", METACORE_SCRIPT_ABI_MAJOR}, + {"abi_minor", METACORE_SCRIPT_ABI_MINOR}, {"module_id", loaded->ModuleId}, + {"build_id", result.BuildId}, {"module_file", result.ModulePath.filename().string()}, + {"sha256", *hash}}.dump(2) << '\n'; + result.Success = manifest.good(); + } + } + return result; +} + +std::optional MetaCoreFindLatestScriptModule(const std::filesystem::path& root, std::string_view configuration) { + const auto base = root / "Library" / "ScriptModules" / "Linux-x86_64" / std::string(configuration); if (!std::filesystem::is_directory(base)) return std::nullopt; + std::filesystem::path latest; std::filesystem::file_time_type time{}; std::error_code ec; + for (const auto& entry : std::filesystem::recursive_directory_iterator(base, ec)) if (entry.path().extension() == ".so") { auto t = entry.last_write_time(ec); if (latest.empty() || t > time) { latest = entry.path(); time = t; } } + return latest.empty() ? std::nullopt : std::optional(latest); +} + +} // namespace MetaCore diff --git a/Source/MetaCoreScripting/Public/MetaCoreScripting/MetaCoreScriptAbi.h b/Source/MetaCoreScripting/Public/MetaCoreScripting/MetaCoreScriptAbi.h new file mode 100644 index 0000000..206eb5a --- /dev/null +++ b/Source/MetaCoreScripting/Public/MetaCoreScripting/MetaCoreScriptAbi.h @@ -0,0 +1,130 @@ +#pragma once + +#include +#include + +#if defined(_WIN32) +#define METACORE_SCRIPT_EXPORT __declspec(dllexport) +#else +#define METACORE_SCRIPT_EXPORT __attribute__((visibility("default"))) +#endif + +extern "C" { + +inline constexpr std::uint32_t METACORE_SCRIPT_ABI_MAJOR = 1U; +inline constexpr std::uint32_t METACORE_SCRIPT_ABI_MINOR = 0U; +inline constexpr std::uint64_t METACORE_SCRIPT_CAP_PROPERTIES = 1ULL << 0U; +inline constexpr std::uint64_t METACORE_SCRIPT_CAP_EVENTS = 1ULL << 1U; +inline constexpr std::uint64_t METACORE_SCRIPT_CAP_TIMERS = 1ULL << 2U; +inline constexpr std::uint64_t METACORE_SCRIPT_HOST_CAPABILITIES = + METACORE_SCRIPT_CAP_PROPERTIES | METACORE_SCRIPT_CAP_EVENTS | METACORE_SCRIPT_CAP_TIMERS; + +enum MetaCoreScriptAbiValueType : std::uint32_t { + MetaCoreScriptAbiValue_None = 0, + MetaCoreScriptAbiValue_Bool, + MetaCoreScriptAbiValue_Int, + MetaCoreScriptAbiValue_Float, + MetaCoreScriptAbiValue_Vec3, + MetaCoreScriptAbiValue_String, + MetaCoreScriptAbiValue_AssetRef, + MetaCoreScriptAbiValue_GameObjectRef +}; + +struct MetaCoreScriptObjectHandleV1 { + std::uint64_t WorldGeneration; + std::uint64_t ObjectId; +}; + +struct MetaCoreScriptValueV1 { + std::uint32_t Type; + std::uint32_t Reserved; + std::int64_t IntValue; + double NumberValue; + double Vec3[3]; + MetaCoreScriptObjectHandleV1 ObjectValue; + char Text[256]; +}; + +struct MetaCoreScriptTimeV1 { + std::uint64_t Frame; + double DeltaSeconds; + double FixedDeltaSeconds; + double ElapsedSeconds; + double UnscaledElapsedSeconds; + double TimeScale; +}; + +struct MetaCoreScriptExecutionContextV1; +using MetaCoreScriptTimerCallbackV1 = const char* (*)(void*, const MetaCoreScriptExecutionContextV1*) noexcept; +using MetaCoreScriptEventCallbackV1 = const char* (*)(void*, const MetaCoreScriptExecutionContextV1*, const char*, const MetaCoreScriptValueV1*, std::size_t) noexcept; + +struct MetaCoreScriptHostApiV1 { + std::uint32_t AbiMajor; + std::uint32_t AbiMinor; + std::uint64_t Capabilities; + bool (*IsObjectValid)(void*, MetaCoreScriptObjectHandleV1) noexcept; + bool (*HasComponent)(void*, MetaCoreScriptObjectHandleV1, const char*) noexcept; + bool (*GetProperty)(void*, MetaCoreScriptObjectHandleV1, const char*, const char*, MetaCoreScriptValueV1*) noexcept; + bool (*SetProperty)(void*, MetaCoreScriptObjectHandleV1, const char*, const char*, const MetaCoreScriptValueV1*) noexcept; + bool (*GetField)(void*, MetaCoreScriptObjectHandleV1, std::uint64_t, const char*, MetaCoreScriptValueV1*) noexcept; + bool (*SetField)(void*, MetaCoreScriptObjectHandleV1, std::uint64_t, const char*, const MetaCoreScriptValueV1*) noexcept; + void (*Log)(void*, std::uint32_t, const char*, const char*) noexcept; + bool (*PublishEvent)(void*, const char*, const MetaCoreScriptValueV1*, std::size_t) noexcept; + std::uint64_t (*SubscribeEvent)(void*, MetaCoreScriptObjectHandleV1, std::uint64_t, const char*, MetaCoreScriptEventCallbackV1, void*) noexcept; + void (*UnsubscribeEvent)(void*, std::uint64_t) noexcept; + std::uint64_t (*ScheduleTimer)(void*, MetaCoreScriptObjectHandleV1, std::uint64_t, double, double, bool, MetaCoreScriptTimerCallbackV1, void*) noexcept; + void (*CancelTimer)(void*, std::uint64_t) noexcept; +}; + +struct MetaCoreScriptExecutionContextV1 { + const MetaCoreScriptHostApiV1* Host; + void* HostContext; + MetaCoreScriptObjectHandleV1 Object; + std::uint64_t InstanceId; + const char* ScriptTypeId; + const char* FieldsJson; + MetaCoreScriptTimeV1 Time; +}; + +struct MetaCoreScriptFieldDescriptorV1 { + const char* FieldId; + const char* DisplayName; + std::uint32_t Type; + const char* DefaultJson; +}; + +using MetaCoreScriptCreateInstanceV1 = void* (*)() noexcept; +using MetaCoreScriptDestroyInstanceV1 = void (*)(void*) noexcept; +using MetaCoreScriptLifecycleCallbackV1 = const char* (*)(void*, const MetaCoreScriptExecutionContextV1*) noexcept; +using MetaCoreScriptUpdateCallbackV1 = const char* (*)(void*, const MetaCoreScriptExecutionContextV1*, double) noexcept; + +struct MetaCoreScriptBehaviourDescriptorV1 { + const char* TypeId; + const char* DisplayName; + std::uint32_t SchemaVersion; + std::size_t FieldCount; + const MetaCoreScriptFieldDescriptorV1* Fields; + MetaCoreScriptCreateInstanceV1 Create; + MetaCoreScriptDestroyInstanceV1 Destroy; + MetaCoreScriptLifecycleCallbackV1 OnPlayStart; + MetaCoreScriptUpdateCallbackV1 FixedUpdate; + MetaCoreScriptUpdateCallbackV1 Update; + MetaCoreScriptUpdateCallbackV1 LateUpdate; + MetaCoreScriptLifecycleCallbackV1 OnPlayStop; +}; + +struct MetaCoreScriptModuleApiV1 { + std::uint32_t AbiMajor; + std::uint32_t AbiMinor; + std::uint64_t RequiredCapabilities; + const char* ModuleId; + const char* BuildId; + std::size_t BehaviourCount; + const MetaCoreScriptBehaviourDescriptorV1* Behaviours; + void (*Shutdown)() noexcept; +}; + +using MetaCoreLoadScriptModuleV1Fn = bool (*)(const MetaCoreScriptHostApiV1*, MetaCoreScriptModuleApiV1*) noexcept; + +} // extern "C" + diff --git a/Source/MetaCoreScripting/Public/MetaCoreScripting/MetaCoreScripting.h b/Source/MetaCoreScripting/Public/MetaCoreScripting/MetaCoreScripting.h new file mode 100644 index 0000000..8b0d562 --- /dev/null +++ b/Source/MetaCoreScripting/Public/MetaCoreScripting/MetaCoreScripting.h @@ -0,0 +1,187 @@ +#pragma once + +#include "MetaCoreScripting/MetaCoreScriptAbi.h" +#include "MetaCoreScene/MetaCoreScene.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace MetaCore { + +enum class MetaCoreScriptFieldType { + Bool = 0, + Int, + Float, + Vec3, + String, + AssetRef, + GameObjectRef +}; + +struct MetaCoreScriptFieldDefinition { + std::string Name{}; // Legacy name and fallback field id. + std::string DisplayName{}; + MetaCoreScriptFieldType Type = MetaCoreScriptFieldType::Float; + bool BoolDefault = false; + float FloatDefault = 0.0F; + glm::vec3 Vec3Default{0.0F}; + std::string StringDefault{}; + std::string FieldId{}; + std::int64_t IntDefault = 0; + MetaCoreAssetGuid AssetDefault{}; + MetaCoreId GameObjectDefault = 0; + + [[nodiscard]] std::string_view StableId() const { return FieldId.empty() ? std::string_view(Name) : std::string_view(FieldId); } +}; + +struct MetaCoreScriptTime { + std::uint64_t Frame = 0; + double DeltaSeconds = 0.0; + double FixedDeltaSeconds = 1.0 / 60.0; + double ElapsedSeconds = 0.0; + double UnscaledElapsedSeconds = 0.0; + double TimeScale = 1.0; +}; + +class MetaCoreScriptRuntime; + +struct MetaCoreScriptExecutionContext { + MetaCoreScene& Scene; + MetaCoreGameObject GameObject; + MetaCoreScriptInstanceData& Instance; + MetaCoreScriptRuntime* Runtime = nullptr; + MetaCoreScriptTime Time{}; +}; + +class MetaCoreIScriptBehaviour { +public: + virtual ~MetaCoreIScriptBehaviour() = default; + virtual void OnPlayStart(MetaCoreScriptExecutionContext& context); + virtual void FixedUpdate(MetaCoreScriptExecutionContext& context, double fixedDeltaSeconds); + virtual void Update(MetaCoreScriptExecutionContext& context, double deltaSeconds); + virtual void LateUpdate(MetaCoreScriptExecutionContext& context, double deltaSeconds); + virtual void OnPlayStop(MetaCoreScriptExecutionContext& context); +}; + +struct MetaCoreScriptDefinition { + std::string TypeId{}; + std::string DisplayName{}; + std::vector Fields{}; + std::function()> Factory{}; + std::string ModuleId = "MetaCore.Builtin"; + std::uint32_t SchemaVersion = 1U; +}; + +using MetaCoreScriptLogHandler = std::function; + +class MetaCoreScriptRegistry { +public: + bool RegisterScript(MetaCoreScriptDefinition definition, std::string* error = nullptr); + bool ReplaceModule(std::string_view moduleId, std::vector definitions, std::string* error = nullptr); + void RemoveModule(std::string_view moduleId); + void Clear(); + [[nodiscard]] const MetaCoreScriptDefinition* FindScript(std::string_view typeId) const; + [[nodiscard]] std::vector GetScriptDefinitions() const; + [[nodiscard]] std::unique_ptr CreateBehaviour(std::string_view typeId) const; + [[nodiscard]] std::string BuildDefaultFieldsJson(std::string_view typeId) const; + [[nodiscard]] std::string EnsureFieldsJsonDefaults(std::string_view typeId, std::string_view fieldsJson) const; + +private: + std::vector Definitions_{}; +}; + +void MetaCoreRegisterBuiltinScripts(MetaCoreScriptRegistry& registry); + +struct MetaCoreLoadedScriptModule { + std::string ModuleId{}; + std::string BuildId{}; + std::filesystem::path Path{}; + std::vector Definitions{}; + std::shared_ptr Lifetime{}; +}; + +class MetaCoreScriptModuleLoader { +public: + [[nodiscard]] std::optional LoadCandidate( + const std::filesystem::path& path, + std::string* error = nullptr + ) const; +}; + +class MetaCoreScriptRuntime { +public: + MetaCoreScriptRuntime(MetaCoreScene& scene, MetaCoreScriptRegistry& registry, MetaCoreScriptLogHandler logger = {}); + ~MetaCoreScriptRuntime(); + + void Start(); + void FixedUpdate(double fixedDeltaSeconds); + void Update(double deltaSeconds); + void LateUpdate(double deltaSeconds); + void Stop(); + void SetTimeScale(double scale); + [[nodiscard]] const MetaCoreScriptTime& GetTime() const { return Time_; } + [[nodiscard]] std::uint64_t GetWorldGeneration() const { return WorldGeneration_; } + [[nodiscard]] std::size_t GetFaultedInstanceCount() const; + + using EventHandler = std::function&)>; + std::uint64_t Subscribe(MetaCoreId objectId, std::uint64_t instanceId, std::string topic, EventHandler handler); + void Unsubscribe(std::uint64_t subscriptionId); + void Publish(std::string topic, std::vector fields = {}); + std::uint64_t ScheduleTimer(MetaCoreId objectId, std::uint64_t instanceId, double delay, double interval, bool unscaled, std::function callback); + void CancelTimer(std::uint64_t timerId); + + [[nodiscard]] bool IsHandleValid(MetaCoreScriptObjectHandleV1 handle) const; + [[nodiscard]] bool HasComponent(MetaCoreScriptObjectHandleV1 handle, std::string_view componentType) const; + [[nodiscard]] bool GetProperty(MetaCoreScriptObjectHandleV1 handle, std::string_view componentType, std::string_view propertyId, MetaCoreScriptValueV1& value) const; + [[nodiscard]] bool SetProperty(MetaCoreScriptObjectHandleV1 handle, std::string_view componentType, std::string_view propertyId, const MetaCoreScriptValueV1& value); + [[nodiscard]] bool GetField(MetaCoreScriptObjectHandleV1 handle, std::uint64_t instanceId, std::string_view fieldId, MetaCoreScriptValueV1& value) const; + [[nodiscard]] bool SetField(MetaCoreScriptObjectHandleV1 handle, std::uint64_t instanceId, std::string_view fieldId, const MetaCoreScriptValueV1& value); + void Log(std::uint32_t level, std::string_view category, std::string_view message) const; + +private: + struct Impl; + std::unique_ptr Impl_; + MetaCoreScriptTime Time_{}; + std::uint64_t WorldGeneration_ = 1; +}; + +struct MetaCoreScriptProjectBuildResult { + bool Success = false; + std::filesystem::path ModulePath{}; + std::filesystem::path ManifestPath{}; + std::filesystem::path BuildDirectory{}; + std::string BuildId{}; + std::string Output{}; +}; + +[[nodiscard]] bool MetaCoreGenerateScriptProject( + const std::filesystem::path& projectRoot, + std::string_view projectName, + const std::filesystem::path& sdkRoot, + std::string* error = nullptr +); + +[[nodiscard]] bool MetaCoreCreateScriptBehaviour( + const std::filesystem::path& projectRoot, + std::string_view projectName, + std::string_view behaviourName, + std::string* error = nullptr +); + +[[nodiscard]] MetaCoreScriptProjectBuildResult MetaCoreBuildScriptProject( + const std::filesystem::path& projectRoot, + std::string_view configuration, + const std::filesystem::path& sdkRoot +); + +[[nodiscard]] std::optional MetaCoreFindLatestScriptModule( + const std::filesystem::path& projectRoot, + std::string_view configuration = "Debug" +); + +} // namespace MetaCore diff --git a/docs/designs/metacore-mature-engine-gap-and-work-plan.md b/docs/designs/metacore-mature-engine-gap-and-work-plan.md index 85df319..7ab962d 100644 --- a/docs/designs/metacore-mature-engine-gap-and-work-plan.md +++ b/docs/designs/metacore-mature-engine-gap-and-work-plan.md @@ -127,6 +127,20 @@ MetaCore 已经超过单纯编辑器 Demo 阶段,当前具备: ### 3. 用户逻辑开发闭环不完整 +#### Linux 首期实施状态(2026-07-15) + +- [x] 路线锁定为 Linux x86_64 原生 C++ 项目插件;嵌入式语言、Windows 对等后端和编辑器内调试器不在首期范围。 +- [x] 新增 Editor / Player 共用的 `MetaCoreScripting` 运行时,统一生命周期、字段、时间、事件和定时器执行。 +- [x] 建立版本化 C ABI、Capability 协商、不透明对象句柄和受控组件/字段访问,动态库边界不暴露 STL、EnTT 或编辑器上下文。 +- [x] 建立脚本工程和行为模板、Clang 15/libc++ 构建、唯一 Build ID 输出、`compile_commands.json` 及 VS Code/LLDB Attach 配置。 +- [x] Inspector 提供生成工程、创建行为、后台编译和加载入口;Playing/Paused 时新模块排队,Stop 回到 Edit 后再原子替换。 +- [x] 候选模块支持 ABI、Schema、TypeId、Manifest 和 SHA-256 校验;失败时保留旧模块和场景字段。 +- [x] 字段支持稳定 Field ID、旧名称迁移、Bool/Int/Float/Vec3/String/AssetRef/GameObjectRef,并保留未知或失效引用数据。 +- [x] Player 加载同一脚本模块并执行同一生命周期;发布流水线构建、Stage、记录、校验模块与独立调试符号。 +- [x] 增加独立 `MetaCoreScriptingTests`,覆盖字段迁移、失败回退、句柄失效、异常隔离、事件重入、定时器清理及真实插件生成/编译/加载。 +- [ ] 在安装 LLDB/CodeLLDB 的开发环境完成 Editor 与 Player 用户代码断点、调用栈和单步人工验收。 +- [ ] 使用包含项目脚本的正式 clean Ubuntu 22.04 发布包完成独立 Player 实机验收。 + #### 问题 - 当前原生 Script Registry 能承载内置行为,但还不是面向项目开发者的成熟脚本系统。 @@ -137,14 +151,14 @@ MetaCore 已经超过单纯编辑器 Demo 阶段,当前具备: #### 需要做的事 -- [ ] 明确第一阶段脚本路线:原生 C++ 插件、嵌入式脚本语言,或二者的阶段关系。 -- [ ] 建立项目脚本模板、构建目标和编辑器编译入口。 -- [ ] 支持脚本模块加载、卸载和失败回退。 -- [ ] 定义脚本 API 稳定边界和对象/组件句柄失效规则。 -- [ ] 支持可序列化字段、资源引用和 GameObject 引用。 -- [ ] 增加统一时间、定时器和事件系统。 -- [ ] 增加脚本错误定位、日志上下文和运行时异常隔离。 -- [ ] 在明确安全边界后实现脚本热重载。 +- [x] 明确第一阶段脚本路线:Linux x86_64 原生 C++ 项目插件;嵌入式语言后续评估。 +- [x] 建立项目脚本模板、构建目标和编辑器编译入口。 +- [x] 支持脚本模块加载、卸载和失败回退。 +- [x] 定义脚本 API 稳定边界和对象/组件句柄失效规则。 +- [x] 支持可序列化字段、资源引用和 GameObject 引用。 +- [x] 增加统一时间、定时器和事件系统。 +- [x] 增加脚本错误定位、日志上下文和运行时异常隔离。 +- [x] 在明确安全边界后实现仅 Edit 态提交的脚本热重载。 #### 验收标准 diff --git a/tests/MetaCoreDeliveryTests.cpp b/tests/MetaCoreDeliveryTests.cpp index eca3deb..79686e1 100644 --- a/tests/MetaCoreDeliveryTests.cpp +++ b/tests/MetaCoreDeliveryTests.cpp @@ -51,6 +51,8 @@ int main() { MetaCore::MetaCoreRuntimeDeliveryDocument delivery; delivery.BuildId = "test-build"; delivery.StartupScene = "payload.bin"; + delivery.ScriptModules.push_back("Project/Scripts/TestScripts.so"); + WriteText(package / delivery.ScriptModules.front(), "script"); Expect(MetaCore::MetaCoreWriteRuntimeDeliveryDocument(package / "Project" / "MetaCore.runtimeproject", delivery), "write runtime delivery"); MetaCore::MetaCoreReleaseManifestDocument manifest; manifest.BuildId = delivery.BuildId; @@ -59,8 +61,12 @@ int main() { manifest.Files.push_back({"payload.bin", "asset", 7U, payloadHash.value_or(""), true}); manifest.Files.push_back({"Project/MetaCore.runtimeproject", "runtime_project", std::filesystem::file_size(package / "Project" / "MetaCore.runtimeproject"), deliveryHash.value_or(""), true}); + const auto scriptHash = MetaCore::MetaCoreSha256File(package / delivery.ScriptModules.front()); + manifest.Files.push_back({delivery.ScriptModules.front(), "script_module", 6U, scriptHash.value_or(""), true}); Expect(MetaCore::MetaCoreWriteReleaseManifest(package / "Manifest" / "MetaCore.release.json", manifest), "write release manifest"); Expect(MetaCore::MetaCoreValidateReleasePackage(package).empty(), "valid package accepted"); + const auto readDelivery = MetaCore::MetaCoreReadRuntimeDeliveryDocument(package / "Project" / "MetaCore.runtimeproject"); + Expect(readDelivery.has_value() && readDelivery->ScriptModules == delivery.ScriptModules, "runtime delivery preserves script module paths"); WriteText(package / "payload.bin", "tampered"); const auto corruptIssues = MetaCore::MetaCoreValidateReleasePackage(package); Expect(!corruptIssues.empty(), "corrupt package rejected"); diff --git a/tests/MetaCoreScriptingTests.cpp b/tests/MetaCoreScriptingTests.cpp new file mode 100644 index 0000000..eff3c97 --- /dev/null +++ b/tests/MetaCoreScriptingTests.cpp @@ -0,0 +1,158 @@ +#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; + } +}