脚本优化
This commit is contained in:
parent
2600ea21b4
commit
204ece778f
@ -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();
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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>();
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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",
|
||||
|
||||
736
Source/MetaCoreScripting/Private/MetaCoreScripting.cpp
Normal file
736
Source/MetaCoreScripting/Private/MetaCoreScripting.cpp
Normal 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
|
||||
@ -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"
|
||||
|
||||
@ -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
|
||||
@ -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 态提交的脚本热重载。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
|
||||
@ -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");
|
||||
|
||||
158
tests/MetaCoreScriptingTests.cpp
Normal file
158
tests/MetaCoreScriptingTests.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user