MetaCore/Source/MetaCoreScripting/Private/MetaCoreScripting.cpp
2026-07-16 09:01:25 +08:00

737 lines
50 KiB
C++

#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