脚本优化

This commit is contained in:
Rowland 2026-07-16 09:01:25 +08:00
parent 2600ea21b4
commit 204ece778f
15 changed files with 1685 additions and 406 deletions

View File

@ -12,6 +12,7 @@
#include "MetaCoreRuntimeUi/MetaCoreRuntimeUiSystem.h"
#include "MetaCoreScene/MetaCoreScenePackage.h"
#include "MetaCoreScene/MetaCoreScene.h"
#include "MetaCoreScripting/MetaCoreScripting.h"
#include <filesystem>
#include <fstream>
@ -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<MetaCore::MetaCoreLoadedScriptModule> loadedScriptModules;
std::vector<std::filesystem::path> 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<MetaCore::MetaCoreScriptComponent>()) continue;
for (const auto& instance : object.GetComponent<MetaCore::MetaCoreScriptComponent>().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();

View File

@ -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

View File

@ -5,6 +5,7 @@
#include "MetaCoreFoundation/MetaCoreProject.h"
#include "MetaCoreScene/MetaCoreScenePackage.h"
#include "MetaCoreScene/MetaCoreSceneSerializer.h"
#include "MetaCoreScripting/MetaCoreScripting.h"
#include <nlohmann/json.hpp>
@ -344,6 +345,11 @@ std::optional<MetaCoreRuntimeDeliveryDocument> 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<std::string>());
}
}
if (document.FormatVersion != GMetaCoreRuntimeDeliveryVersion) {
SetIssue(issue, MetaCoreBuildErrorCode::ManifestVersionUnsupported, "Runtime delivery format is unsupported", path);
return std::nullopt;
@ -358,16 +364,24 @@ std::optional<MetaCoreRuntimeDeliveryDocument> 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<MetaCoreBuildIssue> 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<std::filesystem::path> 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");

View File

@ -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<std::filesystem::path> ScriptModules{};
};
struct MetaCoreReleaseFileEntry {

View File

@ -41,6 +41,7 @@
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <future>
#include <iomanip>
#include <iostream>
#include <limits>
@ -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<MetaCoreIProjectScriptService>();
projectScripts != nullptr) {
static std::array<char, 96> 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<MetaCoreScriptDefinition> 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<char, 256> 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<char, 64> 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<MetaCoreTransformComponent>().RotationEulerDegrees.y +=
degreesPerSecond * static_cast<float>(deltaSeconds);
context.EditorContext.GetScene().IncrementRevision();
context.Scene.IncrementRevision();
}
};
@ -10024,7 +10101,7 @@ public:
context.GameObject.GetComponent<MetaCoreTransformComponent>().Position +=
velocity * static_cast<float>(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<MetaCoreScriptDefinition> GetScriptDefinitions() const override {
return Definitions_;
return Registry_.GetScriptDefinitions();
}
[[nodiscard]] std::unique_ptr<MetaCoreIScriptBehaviour> 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<MetaCoreScriptDefinition> 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<MetaCoreIAssetDatabaseService>();
Registry_ = moduleRegistry.ResolveService<MetaCoreIScriptRegistry>();
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<MetaCoreIAssetDatabaseService> AssetDatabase_{};
std::shared_ptr<MetaCoreIScriptRegistry> Registry_{};
MetaCoreScriptModuleLoader Loader_{};
std::future<MetaCoreScriptProjectBuildResult> BuildFuture_{};
std::optional<MetaCoreLoadedScriptModule> Pending_{};
std::optional<MetaCoreLoadedScriptModule> 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<MetaCoreIScriptRegistry>();
if (registry == nullptr) return;
Runtime_ = std::make_unique<MetaCoreScriptRuntime>(
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<MetaCoreIScriptBehaviour> Behaviour{};
bool Started = false;
};
[[nodiscard]] static MetaCoreScriptInstanceData* FindScriptInstance(MetaCoreGameObject& gameObject, std::uint64_t instanceId) {
if (!gameObject || !gameObject.HasComponent<MetaCoreScriptComponent>()) {
return nullptr;
}
MetaCoreScriptComponent& scriptComponent = gameObject.GetComponent<MetaCoreScriptComponent>();
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, "<empty>");
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<MetaCoreIScriptRegistry>();
if (scriptRegistry == nullptr || scriptRegistry->FindScript(instance.ScriptTypeId) == nullptr) {
WarnMissingScript(editorContext, instance.ScriptTypeId);
return nullptr;
}
std::unique_ptr<MetaCoreIScriptBehaviour> 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<MetaCoreScriptComponent>()) {
continue;
}
MetaCoreScriptComponent& scriptComponent = gameObject.GetComponent<MetaCoreScriptComponent>();
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<RuntimeEntry> RuntimeEntries_{};
std::unordered_set<std::string> MissingScriptWarnings_{};
std::unique_ptr<MetaCoreScriptRuntime> Runtime_{};
};
class MetaCoreRotatorPlayModeComponentSystem final : public MetaCoreIPlayModeComponentSystem {
@ -12026,6 +11971,7 @@ public:
moduleRegistry.RegisterService<MetaCoreIComponentTypeRegistry>(std::make_shared<MetaCoreBuiltinComponentTypeRegistry>());
moduleRegistry.RegisterService<MetaCoreIPlayModeComponentSystemRegistry>(std::make_shared<MetaCoreBuiltinPlayModeComponentSystemRegistry>());
moduleRegistry.RegisterService<MetaCoreIPlayModeService>(std::make_shared<MetaCoreBuiltinPlayModeService>());
moduleRegistry.RegisterService<MetaCoreIProjectScriptService>(std::make_shared<MetaCoreBuiltinProjectScriptService>());
const auto assetDatabase = moduleRegistry.ResolveService<MetaCoreIAssetDatabaseService>();
const auto importPipeline = moduleRegistry.ResolveService<MetaCoreIImportPipelineService>();

View File

@ -2421,6 +2421,11 @@ int MetaCoreEditorApp::Run() {
hotReloadService != nullptr) {
hotReloadService->Tick();
}
if (const auto projectScriptService = ModuleRegistry_.ResolveService<MetaCoreIProjectScriptService>();
projectScriptService != nullptr) {
const auto playModeService = ModuleRegistry_.ResolveService<MetaCoreIPlayModeService>();
projectScriptService->Tick(playModeService == nullptr || playModeService->GetState() == MetaCorePlayModeState::Edit);
}
const auto currentPlayModeTickTime = std::chrono::steady_clock::now();
const double playModeDeltaSeconds =
std::chrono::duration<double>(currentPlayModeTickTime - previousPlayModeTickTime).count();

View File

@ -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;
}

View File

@ -7,6 +7,7 @@
#include "MetaCoreFoundation/MetaCorePackage.h"
#include "MetaCoreFoundation/MetaCoreReflection.h"
#include "MetaCoreScene/MetaCoreScene.h"
#include "MetaCoreScripting/MetaCoreScripting.h"
#include <cstddef>
#include <chrono>
@ -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<MetaCoreScriptFieldDefinition> Fields{};
std::function<std::unique_ptr<MetaCoreIScriptBehaviour>()> Factory{};
};
class MetaCoreIScriptRegistry : public MetaCoreIEditorService {
public:
virtual void RegisterScript(MetaCoreScriptDefinition definition) = 0;
@ -464,6 +424,31 @@ public:
[[nodiscard]] virtual std::unique_ptr<MetaCoreIScriptBehaviour> 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 {

View File

@ -161,7 +161,7 @@ bool MetaCoreCreateProjectSkeleton(
}
std::error_code errorCode;
const std::array<std::filesystem::path, 11> directories = {
const std::array<std::filesystem::path, 12> 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",

View File

@ -0,0 +1,736 @@
#include "MetaCoreScripting/MetaCoreScripting.h"
#include "MetaCoreFoundation/MetaCoreHash.h"
#include <nlohmann/json.hpp>
#include <algorithm>
#include <array>
#include <chrono>
#include <cctype>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <sstream>
#include <stdexcept>
#include <unordered_set>
#include <utility>
#if defined(__linux__)
#include <dlfcn.h>
#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<bool>() ? 1 : 0;
} else if (json.is_number_integer() || json.is_number_unsigned()) {
value.Type = MetaCoreScriptAbiValue_Int;
value.IntValue = json.get<std::int64_t>();
} else if (json.is_number()) {
value.Type = MetaCoreScriptAbiValue_Float;
value.NumberValue = json.get<double>();
} else if (json.is_array() && json.size() >= 3) {
value.Type = MetaCoreScriptAbiValue_Vec3;
value.Vec3[0] = json[0].get<double>();
value.Vec3[1] = json[1].get<double>();
value.Vec3[2] = json[2].get<double>();
} else if (json.is_string()) {
value.Type = MetaCoreScriptAbiValue_String;
std::snprintf(value.Text, sizeof(value.Text), "%s", json.get_ref<const std::string&>().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<std::streamsize>(text.size()));
return output.good();
}
std::string SafeIdentifier(std::string_view value) {
std::string result;
for (char c : value) result += std::isalnum(static_cast<unsigned char>(c)) ? c : '_';
return result.empty() ? "MetaCoreProject" : result;
}
MetaCoreScriptInstanceData* FindInstance(MetaCoreGameObject object, std::uint64_t instanceId) {
if (!object || !object.HasComponent<MetaCoreScriptComponent>()) return nullptr;
auto& instances = object.GetComponent<MetaCoreScriptComponent>().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<LoadedLibrary> 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<LoadedLibrary> 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<std::string> 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<MetaCoreScriptDefinition> definitions, std::string* error) {
std::unordered_set<std::string> 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<MetaCoreScriptDefinition> MetaCoreScriptRegistry::GetScriptDefinitions() const { return Definitions_; }
std::unique_ptr<MetaCoreIScriptBehaviour> 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<MetaCoreTransformComponent>()) return;
context.GameObject.GetComponent<MetaCoreTransformComponent>().RotationEulerDegrees.y +=
fields.value("DegreesPerSecond", 90.0F) * static_cast<float>(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<MetaCoreTransformComponent>()) 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<float>(), fields["Velocity"][1].get<float>(), fields["Velocity"][2].get<float>()};
context.GameObject.GetComponent<MetaCoreTransformComponent>().Position += velocity * static_cast<float>(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<BuiltinRotator>(); };
(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<BuiltinMover>(); };
(void)registry.RegisterScript(std::move(mover));
}
std::optional<MetaCoreLoadedScriptModule> 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<LoadedLibrary>();
library->Handle = dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL);
if (!library->Handle) {
if (error) *error = dlerror();
return std::nullopt;
}
auto load = reinterpret_cast<MetaCoreLoadScriptModuleV1Fn>(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<std::string> 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<std::string> 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<bool>();
else if (field.Type == MetaCoreScriptFieldType::Int && defaultValue.is_number_integer()) field.IntDefault = defaultValue.get<std::int64_t>();
else if (field.Type == MetaCoreScriptFieldType::Float && defaultValue.is_number()) field.FloatDefault = defaultValue.get<float>();
else if (field.Type == MetaCoreScriptFieldType::Vec3 && defaultValue.is_array() && defaultValue.size() >= 3) field.Vec3Default = {defaultValue[0].get<float>(), defaultValue[1].get<float>(), defaultValue[2].get<float>()};
else if ((field.Type == MetaCoreScriptFieldType::String || field.Type == MetaCoreScriptFieldType::AssetRef) && defaultValue.is_string()) field.StringDefault = defaultValue.get<std::string>();
else if (field.Type == MetaCoreScriptFieldType::GameObjectRef && defaultValue.is_number_unsigned()) field.GameObjectDefault = defaultValue.get<MetaCoreId>();
definition.Fields.push_back(std::move(field));
}
definition.Factory = [library, descriptor]() { return std::make_unique<AbiBehaviour>(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<MetaCoreIScriptBehaviour> Behaviour; bool Started = false; bool Faulted = false; };
struct Event { std::string Topic; std::vector<MetaCoreScriptValueV1> 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<void()> Callback; };
MetaCoreScriptRuntime& Owner;
MetaCoreScene& Scene;
MetaCoreScriptRegistry& Registry;
MetaCoreScriptLogHandler Logger;
std::vector<Entry> Entries;
std::vector<Event> Events;
std::vector<Subscription> Subscriptions;
std::vector<Timer> 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<MetaCoreScriptComponent>()) continue;
for (auto& instance : object.GetComponent<MetaCoreScriptComponent>().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<std::uint64_t> 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<Impl>(*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<MetaCoreScriptComponent>()) for (auto& i : o.GetComponent<MetaCoreScriptComponent>().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<MetaCoreScriptValueV1> 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<void()> 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<bool>(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<MetaCoreScriptComponent>();
if (component == "Camera") return object.HasComponent<MetaCoreCameraComponent>();
if (component == "Light") return object.HasComponent<MetaCoreLightComponent>();
if (component == "MeshRenderer") return object.HasComponent<MetaCoreMeshRendererComponent>();
if (component == "Animator") return object.HasComponent<MetaCoreAnimatorComponent>();
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<MetaCoreTransformComponent>(); 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<MetaCoreTransformComponent>(); 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<float>(value.Vec3[0]), static_cast<float>(value.Vec3[1]), static_cast<float>(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<std::uint64_t>(json[std::string(field)].get<MetaCoreId>())};
}
}
}
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<MetaCoreScriptRuntime*>(c)->IsHandleValid(h); } catch (...) { return false; } }
bool HHas(void* c, MetaCoreScriptObjectHandleV1 h, const char* t) noexcept { try { return c && t && static_cast<MetaCoreScriptRuntime*>(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<MetaCoreScriptRuntime*>(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<MetaCoreScriptRuntime*>(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<MetaCoreScriptRuntime*>(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<MetaCoreScriptRuntime*>(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<MetaCoreScriptRuntime*>(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<MetaCoreScriptValueV1> copied(values, values + count); static_cast<MetaCoreScriptRuntime*>(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<MetaCoreScriptRuntime*>(c); return runtime->Subscribe(h.ObjectId, instance, topic, [runtime, h, instance, cb, user](std::string_view name, const std::vector<MetaCoreScriptValueV1>& 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<MetaCoreScriptRuntime*>(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<MetaCoreScriptRuntime*>(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<MetaCoreScriptRuntime*>(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 <MetaCoreScripting/MetaCoreScriptAbi.h>\n#include <exception>\n#include <string>\n#include <vector>\nnamespace MetaCoreScriptSdk { inline std::vector<const MetaCoreScriptBehaviourDescriptorV1*>& Registry(){ static std::vector<const MetaCoreScriptBehaviourDescriptorV1*> value; return value; } struct Registrar { explicit Registrar(const MetaCoreScriptBehaviourDescriptorV1* value){ Registry().push_back(value); } }; template<class F> 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 <MetaCoreScripting/MetaCoreScriptSdk.h>\n#include <vector>\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<MetaCoreScriptBehaviourDescriptorV1> 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 <MetaCoreScripting/MetaCoreScriptSdk.h>\n#include <new>\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<std::chrono::milliseconds>(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<std::filesystem::path> 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<std::filesystem::path>(latest);
}
} // namespace MetaCore

View File

@ -0,0 +1,130 @@
#pragma once
#include <cstddef>
#include <cstdint>
#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"

View File

@ -0,0 +1,187 @@
#pragma once
#include "MetaCoreScripting/MetaCoreScriptAbi.h"
#include "MetaCoreScene/MetaCoreScene.h"
#include <filesystem>
#include <functional>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <vector>
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<MetaCoreScriptFieldDefinition> Fields{};
std::function<std::unique_ptr<MetaCoreIScriptBehaviour>()> Factory{};
std::string ModuleId = "MetaCore.Builtin";
std::uint32_t SchemaVersion = 1U;
};
using MetaCoreScriptLogHandler = std::function<void(std::uint32_t, std::string_view, std::string_view)>;
class MetaCoreScriptRegistry {
public:
bool RegisterScript(MetaCoreScriptDefinition definition, std::string* error = nullptr);
bool ReplaceModule(std::string_view moduleId, std::vector<MetaCoreScriptDefinition> 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<MetaCoreScriptDefinition> GetScriptDefinitions() const;
[[nodiscard]] std::unique_ptr<MetaCoreIScriptBehaviour> 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<MetaCoreScriptDefinition> Definitions_{};
};
void MetaCoreRegisterBuiltinScripts(MetaCoreScriptRegistry& registry);
struct MetaCoreLoadedScriptModule {
std::string ModuleId{};
std::string BuildId{};
std::filesystem::path Path{};
std::vector<MetaCoreScriptDefinition> Definitions{};
std::shared_ptr<void> Lifetime{};
};
class MetaCoreScriptModuleLoader {
public:
[[nodiscard]] std::optional<MetaCoreLoadedScriptModule> 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<void(std::string_view, const std::vector<MetaCoreScriptValueV1>&)>;
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<MetaCoreScriptValueV1> fields = {});
std::uint64_t ScheduleTimer(MetaCoreId objectId, std::uint64_t instanceId, double delay, double interval, bool unscaled, std::function<void()> 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> 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<std::filesystem::path> MetaCoreFindLatestScriptModule(
const std::filesystem::path& projectRoot,
std::string_view configuration = "Debug"
);
} // namespace MetaCore

View File

@ -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 态提交的脚本热重载。
#### 验收标准

View File

@ -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");

View File

@ -0,0 +1,158 @@
#include "MetaCoreScripting/MetaCoreScripting.h"
#include <nlohmann/json.hpp>
#include <cmath>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <memory>
#include <stdexcept>
#include <string>
#include <vector>
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) : 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> 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<CountingBehaviour>(std::make_shared<Counters>()); };
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<MetaCore::MetaCoreScriptComponent>();
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<Counters>();
MetaCore::MetaCoreScriptRegistry registry;
MetaCore::MetaCoreScriptDefinition count;
count.TypeId = "Tests.Count"; count.Factory = [counters] { return std::make_unique<CountingBehaviour>(counters); };
MetaCore::MetaCoreScriptDefinition fault;
fault.TypeId = "Tests.Fault"; fault.Factory = [] { return std::make_unique<FaultBehaviour>(); };
Expect(registry.RegisterScript(std::move(count)), "count script registration");
Expect(registry.RegisterScript(std::move(fault)), "fault script registration");
std::vector<std::string> 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<std::string> 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<std::string>{"first"}, "events published during dispatch should wait for the next batch");
runtime.Update(0.1);
Expect(eventOrder == std::vector<std::string>({"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;
}
}