#include "MetaCoreScripting/MetaCoreScripting.h" #include "MetaCoreFoundation/MetaCoreHash.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if defined(__linux__) #include #endif namespace MetaCore { namespace { using Json = nlohmann::json; Json ParseObject(std::string_view text) { if (text.empty()) return Json::object(); try { Json value = Json::parse(text); return value.is_object() ? value : Json::object(); } catch (...) { return Json::object(); } } Json DefaultJson(const MetaCoreScriptFieldDefinition& field) { switch (field.Type) { case MetaCoreScriptFieldType::Bool: return field.BoolDefault; case MetaCoreScriptFieldType::Int: return field.IntDefault; case MetaCoreScriptFieldType::Float: return field.FloatDefault; case MetaCoreScriptFieldType::Vec3: return Json::array({field.Vec3Default.x, field.Vec3Default.y, field.Vec3Default.z}); case MetaCoreScriptFieldType::String: return field.StringDefault; case MetaCoreScriptFieldType::AssetRef: return field.AssetDefault.ToString(); case MetaCoreScriptFieldType::GameObjectRef: return field.GameObjectDefault; } return nullptr; } MetaCoreScriptFieldType FromAbiType(std::uint32_t type) { switch (type) { case MetaCoreScriptAbiValue_Bool: return MetaCoreScriptFieldType::Bool; case MetaCoreScriptAbiValue_Int: return MetaCoreScriptFieldType::Int; case MetaCoreScriptAbiValue_Float: return MetaCoreScriptFieldType::Float; case MetaCoreScriptAbiValue_Vec3: return MetaCoreScriptFieldType::Vec3; case MetaCoreScriptAbiValue_String: return MetaCoreScriptFieldType::String; case MetaCoreScriptAbiValue_AssetRef: return MetaCoreScriptFieldType::AssetRef; case MetaCoreScriptAbiValue_GameObjectRef: return MetaCoreScriptFieldType::GameObjectRef; default: return MetaCoreScriptFieldType::String; } } bool JsonToAbi(const Json& json, MetaCoreScriptValueV1& value) { value = {}; if (json.is_boolean()) { value.Type = MetaCoreScriptAbiValue_Bool; value.IntValue = json.get() ? 1 : 0; } else if (json.is_number_integer() || json.is_number_unsigned()) { value.Type = MetaCoreScriptAbiValue_Int; value.IntValue = json.get(); } else if (json.is_number()) { value.Type = MetaCoreScriptAbiValue_Float; value.NumberValue = json.get(); } else if (json.is_array() && json.size() >= 3) { value.Type = MetaCoreScriptAbiValue_Vec3; value.Vec3[0] = json[0].get(); value.Vec3[1] = json[1].get(); value.Vec3[2] = json[2].get(); } else if (json.is_string()) { value.Type = MetaCoreScriptAbiValue_String; std::snprintf(value.Text, sizeof(value.Text), "%s", json.get_ref().c_str()); } else { return false; } return true; } Json AbiToJson(const MetaCoreScriptValueV1& value) { switch (value.Type) { case MetaCoreScriptAbiValue_Bool: return value.IntValue != 0; case MetaCoreScriptAbiValue_Int: return value.IntValue; case MetaCoreScriptAbiValue_Float: return value.NumberValue; case MetaCoreScriptAbiValue_Vec3: return Json::array({value.Vec3[0], value.Vec3[1], value.Vec3[2]}); case MetaCoreScriptAbiValue_GameObjectRef: return value.ObjectValue.ObjectId; case MetaCoreScriptAbiValue_String: case MetaCoreScriptAbiValue_AssetRef: return std::string(value.Text); default: return nullptr; } } std::string ShellQuote(const std::filesystem::path& path) { std::string text = path.string(); std::string result = "'"; for (char c : text) result += c == '\'' ? "'\\''" : std::string(1, c); return result + "'"; } bool WriteIfMissing(const std::filesystem::path& path, std::string_view text) { if (std::filesystem::exists(path)) return true; std::ofstream output(path, std::ios::binary); output.write(text.data(), static_cast(text.size())); return output.good(); } std::string SafeIdentifier(std::string_view value) { std::string result; for (char c : value) result += std::isalnum(static_cast(c)) ? c : '_'; return result.empty() ? "MetaCoreProject" : result; } MetaCoreScriptInstanceData* FindInstance(MetaCoreGameObject object, std::uint64_t instanceId) { if (!object || !object.HasComponent()) return nullptr; auto& instances = object.GetComponent().Instances; const auto iterator = std::find_if(instances.begin(), instances.end(), [instanceId](const auto& instance) { return instance.InstanceId == instanceId; }); return iterator == instances.end() ? nullptr : &*iterator; } const MetaCoreScriptHostApiV1& HostApi(); struct LoadedLibrary { void* Handle = nullptr; MetaCoreScriptModuleApiV1 Api{}; ~LoadedLibrary() { if (Api.Shutdown != nullptr) Api.Shutdown(); #if defined(__linux__) if (Handle != nullptr) dlclose(Handle); #endif } }; class AbiBehaviour final : public MetaCoreIScriptBehaviour { public: AbiBehaviour(std::shared_ptr library, MetaCoreScriptBehaviourDescriptorV1 descriptor) : Library_(std::move(library)), Descriptor_(descriptor), Instance_(Descriptor_.Create != nullptr ? Descriptor_.Create() : nullptr) {} ~AbiBehaviour() override { if (Descriptor_.Destroy != nullptr && Instance_ != nullptr) Descriptor_.Destroy(Instance_); } void OnPlayStart(MetaCoreScriptExecutionContext& context) override { Call(context, Descriptor_.OnPlayStart, "OnPlayStart"); } void FixedUpdate(MetaCoreScriptExecutionContext& context, double delta) override { CallUpdate(context, Descriptor_.FixedUpdate, delta, "FixedUpdate"); } void Update(MetaCoreScriptExecutionContext& context, double delta) override { CallUpdate(context, Descriptor_.Update, delta, "Update"); } void LateUpdate(MetaCoreScriptExecutionContext& context, double delta) override { CallUpdate(context, Descriptor_.LateUpdate, delta, "LateUpdate"); } void OnPlayStop(MetaCoreScriptExecutionContext& context) override { Call(context, Descriptor_.OnPlayStop, "OnPlayStop"); } private: MetaCoreScriptExecutionContextV1 BuildContext(MetaCoreScriptExecutionContext& context) const { return MetaCoreScriptExecutionContextV1{ &HostApi(), context.Runtime, {context.Runtime != nullptr ? context.Runtime->GetWorldGeneration() : 0U, context.GameObject.GetId()}, context.Instance.InstanceId, context.Instance.ScriptTypeId.c_str(), context.Instance.FieldsJson.c_str(), {context.Time.Frame, context.Time.DeltaSeconds, context.Time.FixedDeltaSeconds, context.Time.ElapsedSeconds, context.Time.UnscaledElapsedSeconds, context.Time.TimeScale} }; } void Call(MetaCoreScriptExecutionContext& context, MetaCoreScriptLifecycleCallbackV1 callback, const char* phase) { if (callback == nullptr) return; const auto abiContext = BuildContext(context); const char* error = callback(Instance_, &abiContext); if (error != nullptr && error[0] != '\0') throw std::runtime_error(std::string(phase) + ": " + error); } void CallUpdate(MetaCoreScriptExecutionContext& context, MetaCoreScriptUpdateCallbackV1 callback, double delta, const char* phase) { if (callback == nullptr) return; const auto abiContext = BuildContext(context); const char* error = callback(Instance_, &abiContext, delta); if (error != nullptr && error[0] != '\0') throw std::runtime_error(std::string(phase) + ": " + error); } std::shared_ptr Library_{}; MetaCoreScriptBehaviourDescriptorV1 Descriptor_{}; void* Instance_ = nullptr; }; } // namespace void MetaCoreIScriptBehaviour::OnPlayStart(MetaCoreScriptExecutionContext&) {} void MetaCoreIScriptBehaviour::FixedUpdate(MetaCoreScriptExecutionContext&, double) {} void MetaCoreIScriptBehaviour::Update(MetaCoreScriptExecutionContext&, double) {} void MetaCoreIScriptBehaviour::LateUpdate(MetaCoreScriptExecutionContext&, double) {} void MetaCoreIScriptBehaviour::OnPlayStop(MetaCoreScriptExecutionContext&) {} bool MetaCoreScriptRegistry::RegisterScript(MetaCoreScriptDefinition definition, std::string* error) { if (definition.TypeId.empty() || !definition.Factory) { if (error) *error = "Script TypeId and factory are required"; return false; } std::unordered_set fieldIds; for (const auto& field : definition.Fields) { const std::string id(field.StableId()); if (id.empty() || !fieldIds.insert(id).second) { if (error) *error = "Script fields require unique non-empty field ids"; return false; } } const auto existing = std::find_if(Definitions_.begin(), Definitions_.end(), [&](const auto& value) { return value.TypeId == definition.TypeId; }); if (existing != Definitions_.end()) *existing = std::move(definition); else Definitions_.push_back(std::move(definition)); return true; } bool MetaCoreScriptRegistry::ReplaceModule(std::string_view moduleId, std::vector definitions, std::string* error) { std::unordered_set ids; for (auto& definition : definitions) { if (definition.ModuleId.empty()) definition.ModuleId = std::string(moduleId); if (definition.ModuleId != moduleId || !ids.insert(definition.TypeId).second) { if (error) *error = "Candidate module contains duplicate types or a mismatched module id"; return false; } MetaCoreScriptRegistry validator; if (!validator.RegisterScript(definition, error)) return false; const auto outside = std::find_if(Definitions_.begin(), Definitions_.end(), [&](const auto& current) { return current.TypeId == definition.TypeId && current.ModuleId != moduleId; }); if (outside != Definitions_.end()) { if (error) *error = "Script TypeId conflicts with another module: " + definition.TypeId; return false; } } RemoveModule(moduleId); for (auto& definition : definitions) Definitions_.push_back(std::move(definition)); return true; } void MetaCoreScriptRegistry::RemoveModule(std::string_view moduleId) { std::erase_if(Definitions_, [&](const auto& definition) { return definition.ModuleId == moduleId; }); } void MetaCoreScriptRegistry::Clear() { Definitions_.clear(); } const MetaCoreScriptDefinition* MetaCoreScriptRegistry::FindScript(std::string_view typeId) const { const auto it = std::find_if(Definitions_.begin(), Definitions_.end(), [&](const auto& d) { return d.TypeId == typeId; }); return it == Definitions_.end() ? nullptr : &*it; } std::vector MetaCoreScriptRegistry::GetScriptDefinitions() const { return Definitions_; } std::unique_ptr MetaCoreScriptRegistry::CreateBehaviour(std::string_view typeId) const { const auto* definition = FindScript(typeId); return definition != nullptr && definition->Factory ? definition->Factory() : nullptr; } std::string MetaCoreScriptRegistry::BuildDefaultFieldsJson(std::string_view typeId) const { Json fields = Json::object(); const auto* definition = FindScript(typeId); if (definition) for (const auto& field : definition->Fields) fields[std::string(field.StableId())] = DefaultJson(field); return fields.dump(); } std::string MetaCoreScriptRegistry::EnsureFieldsJsonDefaults(std::string_view typeId, std::string_view fieldsJson) const { Json fields = ParseObject(fieldsJson); const auto* definition = FindScript(typeId); if (definition) for (const auto& field : definition->Fields) { const std::string stable(field.StableId()); if (!fields.contains(stable)) { if (!field.Name.empty() && fields.contains(field.Name)) fields[stable] = fields[field.Name]; else fields[stable] = DefaultJson(field); } if (!field.FieldId.empty() && field.Name != stable) fields.erase(field.Name); } return fields.dump(); } namespace { class BuiltinRotator final : public MetaCoreIScriptBehaviour { public: void Update(MetaCoreScriptExecutionContext& context, double delta) override { auto fields = ParseObject(context.Instance.FieldsJson); if (!fields.value("Enabled", true) || !context.GameObject.HasComponent()) return; context.GameObject.GetComponent().RotationEulerDegrees.y += fields.value("DegreesPerSecond", 90.0F) * static_cast(delta); context.Scene.IncrementRevision(); } }; class BuiltinMover final : public MetaCoreIScriptBehaviour { public: void Update(MetaCoreScriptExecutionContext& context, double delta) override { auto fields = ParseObject(context.Instance.FieldsJson); if (!fields.value("Enabled", true) || !context.GameObject.HasComponent()) return; glm::vec3 velocity(0.0F, 1.0F, 0.0F); if (fields.contains("Velocity") && fields["Velocity"].is_array() && fields["Velocity"].size() >= 3) velocity = {fields["Velocity"][0].get(), fields["Velocity"][1].get(), fields["Velocity"][2].get()}; context.GameObject.GetComponent().Position += velocity * static_cast(delta); context.Scene.IncrementRevision(); } }; } void MetaCoreRegisterBuiltinScripts(MetaCoreScriptRegistry& registry) { MetaCoreScriptDefinition rotator; rotator.TypeId = "MetaCore.Script.Rotator"; rotator.DisplayName = "旋转脚本"; rotator.Fields = { MetaCoreScriptFieldDefinition{"Enabled", "启用", MetaCoreScriptFieldType::Bool, true}, MetaCoreScriptFieldDefinition{"DegreesPerSecond", "角速度 Y (度/秒)", MetaCoreScriptFieldType::Float, false, 90.0F} }; rotator.Factory = [] { return std::make_unique(); }; (void)registry.RegisterScript(std::move(rotator)); MetaCoreScriptDefinition mover; mover.TypeId = "MetaCore.Script.Mover"; mover.DisplayName = "移动脚本"; mover.Fields = { MetaCoreScriptFieldDefinition{"Enabled", "启用", MetaCoreScriptFieldType::Bool, true}, MetaCoreScriptFieldDefinition{"Velocity", "速度", MetaCoreScriptFieldType::Vec3, false, 0.0F, glm::vec3(0.0F, 1.0F, 0.0F)} }; mover.Factory = [] { return std::make_unique(); }; (void)registry.RegisterScript(std::move(mover)); } std::optional MetaCoreScriptModuleLoader::LoadCandidate(const std::filesystem::path& path, std::string* error) const { #if !defined(__linux__) if (error) *error = "Native project scripts are currently supported only on Linux"; return std::nullopt; #else if (!std::filesystem::is_regular_file(path)) { if (error) *error = "Script module does not exist: " + path.string(); return std::nullopt; } std::string manifestModuleId; std::string manifestBuildId; std::filesystem::path manifestPath = path; manifestPath += ".mcscriptmodule.json"; if (std::filesystem::is_regular_file(manifestPath)) { try { std::ifstream manifestInput(manifestPath); Json manifest = Json::parse(manifestInput); const auto hash = MetaCoreSha256File(path); manifestModuleId = manifest.value("module_id", ""); manifestBuildId = manifest.value("build_id", ""); if (manifest.value("format_version", 0U) != 1U || manifest.value("abi_major", 0U) != METACORE_SCRIPT_ABI_MAJOR || manifest.value("abi_minor", 0U) > METACORE_SCRIPT_ABI_MINOR || manifest.value("module_file", "") != path.filename().string() || !hash.has_value() || manifest.value("sha256", "") != *hash) { if (error) *error = "Script module manifest or SHA-256 validation failed"; return std::nullopt; } } catch (...) { if (error) *error = "Script module manifest is unreadable"; return std::nullopt; } } auto library = std::make_shared(); library->Handle = dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL); if (!library->Handle) { if (error) *error = dlerror(); return std::nullopt; } auto load = reinterpret_cast(dlsym(library->Handle, "MetaCoreLoadScriptModuleV1")); if (!load) { if (error) *error = "Module does not export MetaCoreLoadScriptModuleV1"; return std::nullopt; } try { if (!load(&HostApi(), &library->Api)) { if (error) *error = "Module initialization rejected the host"; return std::nullopt; } } catch (...) { if (error) *error = "Module initialization crossed the ABI with an exception"; return std::nullopt; } const auto& api = library->Api; if (api.AbiMajor != METACORE_SCRIPT_ABI_MAJOR || api.AbiMinor > METACORE_SCRIPT_ABI_MINOR || (api.RequiredCapabilities & ~METACORE_SCRIPT_HOST_CAPABILITIES) != 0 || api.ModuleId == nullptr || api.ModuleId[0] == '\0') { if (error) *error = "Module ABI, capabilities, or module id are incompatible"; return std::nullopt; } if (api.BehaviourCount > 0 && api.Behaviours == nullptr) { if (error) *error = "Module behaviour table is null"; return std::nullopt; } MetaCoreLoadedScriptModule result; result.ModuleId = api.ModuleId; if (!manifestModuleId.empty() && manifestModuleId != result.ModuleId) { if (error) *error = "Loaded module id does not match its validated manifest"; return std::nullopt; } result.BuildId = api.BuildId != nullptr ? api.BuildId : ""; result.Path = path; result.Lifetime = library; std::unordered_set typeIds; for (std::size_t i = 0; i < api.BehaviourCount; ++i) { const auto descriptor = api.Behaviours[i]; if (descriptor.TypeId == nullptr || descriptor.TypeId[0] == '\0' || descriptor.Create == nullptr || descriptor.Destroy == nullptr || (descriptor.FieldCount > 0 && descriptor.Fields == nullptr) || !typeIds.insert(descriptor.TypeId).second) { if (error) *error = "Module contains an invalid or duplicate behaviour descriptor"; return std::nullopt; } if (!manifestBuildId.empty() && manifestBuildId != result.BuildId) { if (error) *error = "Loaded module build id does not match its validated manifest"; return std::nullopt; } MetaCoreScriptDefinition definition; definition.TypeId = descriptor.TypeId; definition.DisplayName = descriptor.DisplayName != nullptr ? descriptor.DisplayName : descriptor.TypeId; definition.ModuleId = result.ModuleId; definition.SchemaVersion = descriptor.SchemaVersion; std::unordered_set fieldIds; for (std::size_t fieldIndex = 0; fieldIndex < descriptor.FieldCount; ++fieldIndex) { const auto& source = descriptor.Fields[fieldIndex]; if (source.FieldId == nullptr || source.FieldId[0] == '\0' || !fieldIds.insert(source.FieldId).second) { if (error) *error = "Module contains an invalid or duplicate field id"; return std::nullopt; } MetaCoreScriptFieldDefinition field; field.Name = source.FieldId; field.FieldId = source.FieldId; field.DisplayName = source.DisplayName != nullptr ? source.DisplayName : source.FieldId; field.Type = FromAbiType(source.Type); Json defaultValue; try { defaultValue = source.DefaultJson != nullptr ? Json::parse(source.DefaultJson) : Json{}; } catch (...) { if (error) *error = "Field default JSON is invalid"; return std::nullopt; } if (field.Type == MetaCoreScriptFieldType::Bool && defaultValue.is_boolean()) field.BoolDefault = defaultValue.get(); else if (field.Type == MetaCoreScriptFieldType::Int && defaultValue.is_number_integer()) field.IntDefault = defaultValue.get(); else if (field.Type == MetaCoreScriptFieldType::Float && defaultValue.is_number()) field.FloatDefault = defaultValue.get(); else if (field.Type == MetaCoreScriptFieldType::Vec3 && defaultValue.is_array() && defaultValue.size() >= 3) field.Vec3Default = {defaultValue[0].get(), defaultValue[1].get(), defaultValue[2].get()}; else if ((field.Type == MetaCoreScriptFieldType::String || field.Type == MetaCoreScriptFieldType::AssetRef) && defaultValue.is_string()) field.StringDefault = defaultValue.get(); else if (field.Type == MetaCoreScriptFieldType::GameObjectRef && defaultValue.is_number_unsigned()) field.GameObjectDefault = defaultValue.get(); definition.Fields.push_back(std::move(field)); } definition.Factory = [library, descriptor]() { return std::make_unique(library, descriptor); }; result.Definitions.push_back(std::move(definition)); } return result; #endif } struct MetaCoreScriptRuntime::Impl { struct Entry { MetaCoreId ObjectId = 0; std::uint64_t InstanceId = 0; std::string TypeId; std::unique_ptr Behaviour; bool Started = false; bool Faulted = false; }; struct Event { std::string Topic; std::vector Fields; }; struct Subscription { std::uint64_t Id; MetaCoreId ObjectId; std::uint64_t InstanceId; std::string Topic; EventHandler Handler; }; struct Timer { std::uint64_t Id; MetaCoreId ObjectId; std::uint64_t InstanceId; double Remaining; double Interval; bool Unscaled; std::function Callback; }; MetaCoreScriptRuntime& Owner; MetaCoreScene& Scene; MetaCoreScriptRegistry& Registry; MetaCoreScriptLogHandler Logger; std::vector Entries; std::vector Events; std::vector Subscriptions; std::vector Timers; std::uint64_t NextSubscription = 1; std::uint64_t NextTimer = 1; bool Running = false; Impl(MetaCoreScriptRuntime& owner, MetaCoreScene& scene, MetaCoreScriptRegistry& registry, MetaCoreScriptLogHandler logger) : Owner(owner), Scene(scene), Registry(registry), Logger(std::move(logger)) {} void Log(std::uint32_t level, std::string_view category, std::string_view text) const { if (Logger) Logger(level, category, text); } Entry* FindEntry(MetaCoreId object, std::uint64_t instance) { auto it = std::find_if(Entries.begin(), Entries.end(), [&](const auto& e) { return e.ObjectId == object && e.InstanceId == instance; }); return it == Entries.end() ? nullptr : &*it; } MetaCoreScriptExecutionContext Context(MetaCoreGameObject object, MetaCoreScriptInstanceData& instance) { return {Scene, object, instance, &Owner, Owner.Time_}; } void Fault(Entry& entry, std::string_view phase, std::string_view message) { entry.Faulted = true; std::ostringstream text; text << "module/script callback fault type=\"" << entry.TypeId << "\" object=" << entry.ObjectId << " instance=" << entry.InstanceId << " phase=" << phase << " error=" << message; Log(2, "Script", text.str()); CleanupOwner(entry.ObjectId, entry.InstanceId); } void CleanupOwner(MetaCoreId object, std::uint64_t instance) { std::erase_if(Subscriptions, [&](const auto& s) { return s.ObjectId == object && s.InstanceId == instance; }); std::erase_if(Timers, [&](const auto& t) { return t.ObjectId == object && t.InstanceId == instance; }); } Entry* Ensure(MetaCoreGameObject object, MetaCoreScriptInstanceData& instance) { if (instance.InstanceId == 0) instance.InstanceId = MetaCoreIdGenerator::Generate(); if (auto* current = FindEntry(object.GetId(), instance.InstanceId)) return current; const auto* definition = Registry.FindScript(instance.ScriptTypeId); if (!definition) { Log(1, "Script", "Missing script type: " + instance.ScriptTypeId); return nullptr; } instance.FieldsJson = Registry.EnsureFieldsJsonDefaults(instance.ScriptTypeId, instance.FieldsJson); Entry entry{object.GetId(), instance.InstanceId, instance.ScriptTypeId, Registry.CreateBehaviour(instance.ScriptTypeId)}; if (!entry.Behaviour) return nullptr; Entries.push_back(std::move(entry)); auto& created = Entries.back(); try { auto context = Context(object, instance); created.Behaviour->OnPlayStart(context); created.Started = true; } catch (const std::exception& e) { Fault(created, "OnPlayStart", e.what()); } catch (...) { Fault(created, "OnPlayStart", "unknown exception"); } return &created; } void CleanupStale() { for (auto it = Entries.begin(); it != Entries.end();) { auto object = Scene.FindGameObject(it->ObjectId); auto* instance = FindInstance(object, it->InstanceId); if (!object || !instance || !instance->Enabled || instance->ScriptTypeId != it->TypeId) { StopEntry(*it, object, instance); CleanupOwner(it->ObjectId, it->InstanceId); it = Entries.erase(it); } else ++it; } } void StopEntry(Entry& entry, MetaCoreGameObject object, MetaCoreScriptInstanceData* instance) { if (!entry.Started || !object || !instance) return; try { auto context = Context(object, *instance); entry.Behaviour->OnPlayStop(context); } catch (const std::exception& e) { Log(2, "Script", std::string("OnPlayStop fault: ") + e.what()); } catch (...) { Log(2, "Script", "OnPlayStop fault: unknown exception"); } entry.Started = false; } enum class Phase { Fixed, Update, Late }; void Dispatch(Phase phase, double delta) { CleanupStale(); for (auto object : Scene.GetGameObjects()) { if (!object.HasComponent()) continue; for (auto& instance : object.GetComponent().Instances) { if (!instance.Enabled) continue; auto* entry = Ensure(object, instance); if (!entry || entry->Faulted) continue; try { auto context = Context(object, instance); if (phase == Phase::Fixed) entry->Behaviour->FixedUpdate(context, delta); else if (phase == Phase::Update) entry->Behaviour->Update(context, delta); else entry->Behaviour->LateUpdate(context, delta); } catch (const std::exception& e) { Fault(*entry, phase == Phase::Fixed ? "FixedUpdate" : phase == Phase::Update ? "Update" : "LateUpdate", e.what()); } catch (...) { Fault(*entry, "Update", "unknown exception"); } } } } void DispatchEvents() { auto current = std::move(Events); Events.clear(); for (const auto& event : current) { const auto subscriptions = Subscriptions; for (const auto& subscription : subscriptions) { if (subscription.Topic == event.Topic && FindInstance(Scene.FindGameObject(subscription.ObjectId), subscription.InstanceId)) { try { subscription.Handler(event.Topic, event.Fields); } catch (...) { Log(2, "Script.Event", "Event handler threw an exception"); } } } } } void TickTimers(double scaled, double unscaled) { std::erase_if(Timers, [&](const auto& timer) { return !FindInstance(Scene.FindGameObject(timer.ObjectId), timer.InstanceId); }); std::vector fire; for (auto& timer : Timers) { timer.Remaining -= timer.Unscaled ? unscaled : scaled; if (timer.Remaining <= 0.0) fire.push_back(timer.Id); } for (auto id : fire) { auto it = std::find_if(Timers.begin(), Timers.end(), [id](const auto& t) { return t.Id == id; }); if (it == Timers.end()) continue; auto callback = it->Callback; const double interval = it->Interval; if (interval > 0.0) it->Remaining += interval; else Timers.erase(it); try { callback(); } catch (...) { Log(2, "Script.Timer", "Timer callback threw an exception"); } } } }; MetaCoreScriptRuntime::MetaCoreScriptRuntime(MetaCoreScene& scene, MetaCoreScriptRegistry& registry, MetaCoreScriptLogHandler logger) : Impl_(std::make_unique(*this, scene, registry, std::move(logger))) {} MetaCoreScriptRuntime::~MetaCoreScriptRuntime() { Stop(); } void MetaCoreScriptRuntime::Start() { if (Impl_->Running) return; Impl_->Running = true; ++WorldGeneration_; for (auto o : Impl_->Scene.GetGameObjects()) if (o.HasComponent()) for (auto& i : o.GetComponent().Instances) if (i.Enabled) Impl_->Ensure(o, i); } void MetaCoreScriptRuntime::FixedUpdate(double delta) { if (!Impl_->Running) return; Time_.FixedDeltaSeconds = std::max(0.0, delta); Impl_->Dispatch(Impl::Phase::Fixed, Time_.FixedDeltaSeconds); } void MetaCoreScriptRuntime::Update(double delta) { if (!Impl_->Running) return; const double unscaled = std::max(0.0, delta); const double scaled = unscaled * Time_.TimeScale; ++Time_.Frame; Time_.DeltaSeconds = scaled; Time_.ElapsedSeconds += scaled; Time_.UnscaledElapsedSeconds += unscaled; Impl_->TickTimers(scaled, unscaled); Impl_->DispatchEvents(); Impl_->Dispatch(Impl::Phase::Update, scaled); } void MetaCoreScriptRuntime::LateUpdate(double delta) { if (Impl_->Running) Impl_->Dispatch(Impl::Phase::Late, std::max(0.0, delta) * Time_.TimeScale); } void MetaCoreScriptRuntime::Stop() { if (!Impl_ || !Impl_->Running) return; for (auto& entry : Impl_->Entries) { auto object = Impl_->Scene.FindGameObject(entry.ObjectId); Impl_->StopEntry(entry, object, FindInstance(object, entry.InstanceId)); } Impl_->Entries.clear(); Impl_->Events.clear(); Impl_->Subscriptions.clear(); Impl_->Timers.clear(); Impl_->Running = false; Time_ = {}; ++WorldGeneration_; } void MetaCoreScriptRuntime::SetTimeScale(double scale) { Time_.TimeScale = std::max(0.0, scale); } std::size_t MetaCoreScriptRuntime::GetFaultedInstanceCount() const { return std::count_if(Impl_->Entries.begin(), Impl_->Entries.end(), [](const auto& e) { return e.Faulted; }); } std::uint64_t MetaCoreScriptRuntime::Subscribe(MetaCoreId object, std::uint64_t instance, std::string topic, EventHandler handler) { const auto id = Impl_->NextSubscription++; Impl_->Subscriptions.push_back({id, object, instance, std::move(topic), std::move(handler)}); return id; } void MetaCoreScriptRuntime::Unsubscribe(std::uint64_t id) { std::erase_if(Impl_->Subscriptions, [id](const auto& s) { return s.Id == id; }); } void MetaCoreScriptRuntime::Publish(std::string topic, std::vector fields) { Impl_->Events.push_back({std::move(topic), std::move(fields)}); } std::uint64_t MetaCoreScriptRuntime::ScheduleTimer(MetaCoreId object, std::uint64_t instance, double delay, double interval, bool unscaled, std::function callback) { const auto id = Impl_->NextTimer++; Impl_->Timers.push_back({id, object, instance, std::max(0.0, delay), std::max(0.0, interval), unscaled, std::move(callback)}); return id; } void MetaCoreScriptRuntime::CancelTimer(std::uint64_t id) { std::erase_if(Impl_->Timers, [id](const auto& t) { return t.Id == id; }); } bool MetaCoreScriptRuntime::IsHandleValid(MetaCoreScriptObjectHandleV1 handle) const { return handle.WorldGeneration == WorldGeneration_ && handle.ObjectId != 0 && static_cast(Impl_->Scene.FindGameObject(handle.ObjectId)); } bool MetaCoreScriptRuntime::HasComponent(MetaCoreScriptObjectHandleV1 handle, std::string_view component) const { auto object = IsHandleValid(handle) ? Impl_->Scene.FindGameObject(handle.ObjectId) : MetaCoreGameObject{}; if (!object) return false; if (component == "Transform" || component == "Name") return true; if (component == "Script") return object.HasComponent(); if (component == "Camera") return object.HasComponent(); if (component == "Light") return object.HasComponent(); if (component == "MeshRenderer") return object.HasComponent(); if (component == "Animator") return object.HasComponent(); return false; } bool MetaCoreScriptRuntime::GetProperty(MetaCoreScriptObjectHandleV1 handle, std::string_view component, std::string_view property, MetaCoreScriptValueV1& value) const { if (!IsHandleValid(handle)) return false; auto object = Impl_->Scene.FindGameObject(handle.ObjectId); value = {}; if (component == "Name" && property == "Value") { value.Type = MetaCoreScriptAbiValue_String; std::snprintf(value.Text, sizeof(value.Text), "%s", object.GetName().c_str()); return true; } if (component != "Transform") return false; const auto& transform = object.GetComponent(); const glm::vec3* source = nullptr; if (property == "Position") source = &transform.Position; else if (property == "Rotation") source = &transform.RotationEulerDegrees; else if (property == "Scale") source = &transform.Scale; else return false; value.Type = MetaCoreScriptAbiValue_Vec3; value.Vec3[0] = source->x; value.Vec3[1] = source->y; value.Vec3[2] = source->z; return true; } bool MetaCoreScriptRuntime::SetProperty(MetaCoreScriptObjectHandleV1 handle, std::string_view component, std::string_view property, const MetaCoreScriptValueV1& value) { if (!IsHandleValid(handle)) return false; auto object = Impl_->Scene.FindGameObject(handle.ObjectId); if (component == "Name" && property == "Value" && value.Type == MetaCoreScriptAbiValue_String) { object.GetName() = value.Text; Impl_->Scene.IncrementRevision(); return true; } if (component != "Transform" || value.Type != MetaCoreScriptAbiValue_Vec3) return false; auto& transform = object.GetComponent(); glm::vec3* target = nullptr; if (property == "Position") target = &transform.Position; else if (property == "Rotation") target = &transform.RotationEulerDegrees; else if (property == "Scale") target = &transform.Scale; else return false; *target = {static_cast(value.Vec3[0]), static_cast(value.Vec3[1]), static_cast(value.Vec3[2])}; Impl_->Scene.IncrementRevision(); return true; } bool MetaCoreScriptRuntime::GetField(MetaCoreScriptObjectHandleV1 handle, std::uint64_t instance, std::string_view field, MetaCoreScriptValueV1& value) const { if (!IsHandleValid(handle)) return false; auto* data = FindInstance(Impl_->Scene.FindGameObject(handle.ObjectId), instance); if (!data) return false; auto json = ParseObject(data->FieldsJson); if (!json.contains(std::string(field)) || !JsonToAbi(json[std::string(field)], value)) return false; if (const auto* definition = Impl_->Registry.FindScript(data->ScriptTypeId); definition != nullptr) { const auto fieldDefinition = std::find_if(definition->Fields.begin(), definition->Fields.end(), [&](const auto& candidate) { return candidate.StableId() == field; }); if (fieldDefinition != definition->Fields.end()) { if (fieldDefinition->Type == MetaCoreScriptFieldType::AssetRef) value.Type = MetaCoreScriptAbiValue_AssetRef; if (fieldDefinition->Type == MetaCoreScriptFieldType::GameObjectRef) { value.Type = MetaCoreScriptAbiValue_GameObjectRef; value.ObjectValue = {WorldGeneration_, static_cast(json[std::string(field)].get())}; } } } return true; } bool MetaCoreScriptRuntime::SetField(MetaCoreScriptObjectHandleV1 handle, std::uint64_t instance, std::string_view field, const MetaCoreScriptValueV1& value) { if (!IsHandleValid(handle)) return false; auto* data = FindInstance(Impl_->Scene.FindGameObject(handle.ObjectId), instance); if (!data) return false; auto json = ParseObject(data->FieldsJson); json[std::string(field)] = AbiToJson(value); data->FieldsJson = json.dump(); Impl_->Scene.IncrementRevision(); return true; } void MetaCoreScriptRuntime::Log(std::uint32_t level, std::string_view category, std::string_view message) const { Impl_->Log(level, category, message); } namespace { bool HValid(void* c, MetaCoreScriptObjectHandleV1 h) noexcept { try { return c && static_cast(c)->IsHandleValid(h); } catch (...) { return false; } } bool HHas(void* c, MetaCoreScriptObjectHandleV1 h, const char* t) noexcept { try { return c && t && static_cast(c)->HasComponent(h, t); } catch (...) { return false; } } bool HGet(void* c, MetaCoreScriptObjectHandleV1 h, const char* t, const char* p, MetaCoreScriptValueV1* v) noexcept { try { return c && t && p && v && static_cast(c)->GetProperty(h, t, p, *v); } catch (...) { return false; } } bool HSet(void* c, MetaCoreScriptObjectHandleV1 h, const char* t, const char* p, const MetaCoreScriptValueV1* v) noexcept { try { return c && t && p && v && static_cast(c)->SetProperty(h, t, p, *v); } catch (...) { return false; } } bool HGetField(void* c, MetaCoreScriptObjectHandleV1 h, std::uint64_t i, const char* f, MetaCoreScriptValueV1* v) noexcept { try { return c && f && v && static_cast(c)->GetField(h, i, f, *v); } catch (...) { return false; } } bool HSetField(void* c, MetaCoreScriptObjectHandleV1 h, std::uint64_t i, const char* f, const MetaCoreScriptValueV1* v) noexcept { try { return c && f && v && static_cast(c)->SetField(h, i, f, *v); } catch (...) { return false; } } void HLog(void* c, std::uint32_t l, const char* k, const char* m) noexcept { try { if (c) static_cast(c)->Log(l, k ? k : "Script", m ? m : ""); } catch (...) {} } bool HPublish(void* c, const char* topic, const MetaCoreScriptValueV1* values, std::size_t count) noexcept { try { if (!c || !topic) return false; std::vector copied(values, values + count); static_cast(c)->Publish(topic, std::move(copied)); return true; } catch (...) { return false; } } std::uint64_t HSubscribe(void* c, MetaCoreScriptObjectHandleV1 h, std::uint64_t instance, const char* topic, MetaCoreScriptEventCallbackV1 cb, void* user) noexcept { try { if (!c || !topic || !cb) return 0; auto* runtime = static_cast(c); return runtime->Subscribe(h.ObjectId, instance, topic, [runtime, h, instance, cb, user](std::string_view name, const std::vector& fields) { MetaCoreScriptExecutionContextV1 context{&HostApi(), runtime, h, instance, "", "{}", {}}; if (const char* e = cb(user, &context, std::string(name).c_str(), fields.data(), fields.size()); e) runtime->Log(2, "Script.Event", e); }); } catch (...) { return 0; } } void HUnsubscribe(void* c, std::uint64_t id) noexcept { try { if (c) static_cast(c)->Unsubscribe(id); } catch (...) {} } std::uint64_t HSchedule(void* c, MetaCoreScriptObjectHandleV1 h, std::uint64_t instance, double delay, double interval, bool unscaled, MetaCoreScriptTimerCallbackV1 cb, void* user) noexcept { try { if (!c || !cb) return 0; auto* runtime = static_cast(c); return runtime->ScheduleTimer(h.ObjectId, instance, delay, interval, unscaled, [runtime, h, instance, cb, user] { MetaCoreScriptExecutionContextV1 context{&HostApi(), runtime, h, instance, "", "{}", {}}; if (const char* e = cb(user, &context); e) runtime->Log(2, "Script.Timer", e); }); } catch (...) { return 0; } } void HCancel(void* c, std::uint64_t id) noexcept { try { if (c) static_cast(c)->CancelTimer(id); } catch (...) {} } const MetaCoreScriptHostApiV1& HostApi() { static const MetaCoreScriptHostApiV1 api{METACORE_SCRIPT_ABI_MAJOR, METACORE_SCRIPT_ABI_MINOR, METACORE_SCRIPT_HOST_CAPABILITIES, HValid, HHas, HGet, HSet, HGetField, HSetField, HLog, HPublish, HSubscribe, HUnsubscribe, HSchedule, HCancel}; return api; } } // namespace bool MetaCoreGenerateScriptProject(const std::filesystem::path& root, std::string_view projectName, const std::filesystem::path& sdkRoot, std::string* error) { const auto scripts = root / "Scripts"; std::error_code ec; std::filesystem::create_directories(scripts / "Source", ec); std::filesystem::create_directories(scripts / "SDK" / "MetaCoreScripting", ec); std::filesystem::create_directories(root / ".vscode", ec); const auto sourceHeader = sdkRoot / "Source" / "MetaCoreScripting" / "Public" / "MetaCoreScripting" / "MetaCoreScriptAbi.h"; if (ec || !std::filesystem::is_regular_file(sourceHeader)) { if (error) *error = "MetaCore scripting SDK header was not found at " + sourceHeader.string(); return false; } std::filesystem::copy_file(sourceHeader, scripts / "SDK" / "MetaCoreScripting" / "MetaCoreScriptAbi.h", std::filesystem::copy_options::overwrite_existing, ec); if (ec) { if (error) *error = ec.message(); return false; } const std::string id = SafeIdentifier(projectName); const std::string cmake = "cmake_minimum_required(VERSION 3.20)\nproject(" + id + "Scripts LANGUAGES CXX)\nif(NOT CMAKE_SYSTEM_NAME STREQUAL \"Linux\" OR NOT CMAKE_SIZEOF_VOID_P EQUAL 8)\n message(FATAL_ERROR \"MetaCore script v1 requires Linux x86_64\")\nendif()\nif(NOT CMAKE_CXX_COMPILER_ID MATCHES \"Clang\")\n message(FATAL_ERROR \"MetaCore script v1 requires Clang and libc++\")\nendif()\nset(CMAKE_CXX_STANDARD 20)\nset(CMAKE_POSITION_INDEPENDENT_CODE ON)\nfile(GLOB SCRIPT_SOURCES CONFIGURE_DEPENDS Source/*.cpp)\nadd_library(" + id + "Scripts SHARED ${SCRIPT_SOURCES})\ntarget_include_directories(" + id + "Scripts PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/SDK)\ntarget_compile_definitions(" + id + "Scripts PRIVATE METACORE_SCRIPT_MODULE_ID=\\\"" + id + "\\\" METACORE_SCRIPT_BUILD_ID=\\\"${METACORE_SCRIPT_BUILD_ID}\\\")\nset_target_properties(" + id + "Scripts PROPERTIES PREFIX \"\" OUTPUT_NAME \"" + id + "Scripts_${METACORE_SCRIPT_BUILD_ID}\" LIBRARY_OUTPUT_DIRECTORY \"${METACORE_SCRIPT_OUTPUT_DIR}\")\n"; const std::string sdk = "#pragma once\n#include \n#include \n#include \n#include \nnamespace MetaCoreScriptSdk { inline std::vector& Registry(){ static std::vector value; return value; } struct Registrar { explicit Registrar(const MetaCoreScriptBehaviourDescriptorV1* value){ Registry().push_back(value); } }; template const char* Guard(F&& function) noexcept { try { function(); return nullptr; } catch(const std::exception& e) { static thread_local std::string error; error=e.what(); return error.c_str(); } catch(...) { return \"unknown C++ exception\"; } } }\n#define MC_REGISTER_SCRIPT(Name, Descriptor) static MetaCoreScriptSdk::Registrar Name##Registrar(&(Descriptor))\n"; const std::string module = "#include \n#include \nextern \"C\" METACORE_SCRIPT_EXPORT bool MetaCoreLoadScriptModuleV1(const MetaCoreScriptHostApiV1* host, MetaCoreScriptModuleApiV1* out) noexcept { if(!host||!out||host->AbiMajor!=METACORE_SCRIPT_ABI_MAJOR) return false; static std::vector descriptors; if(descriptors.empty()) for(const auto* descriptor:MetaCoreScriptSdk::Registry()) descriptors.push_back(*descriptor); *out={METACORE_SCRIPT_ABI_MAJOR,METACORE_SCRIPT_ABI_MINOR,METACORE_SCRIPT_CAP_PROPERTIES|METACORE_SCRIPT_CAP_EVENTS|METACORE_SCRIPT_CAP_TIMERS,METACORE_SCRIPT_MODULE_ID,METACORE_SCRIPT_BUILD_ID,descriptors.size(),descriptors.data(),nullptr}; return true; }\n"; const std::string launch = "{\n \"version\": \"0.2.0\",\n \"configurations\": [\n {\"name\": \"Attach MetaCore Editor (LLDB)\", \"type\": \"lldb\", \"request\": \"attach\", \"pid\": \"${command:pickMyProcess}\"},\n {\"name\": \"Attach MetaCore Player (LLDB)\", \"type\": \"lldb\", \"request\": \"attach\", \"pid\": \"${command:pickMyProcess}\"}\n ]\n}\n"; if (!WriteIfMissing(scripts / "CMakeLists.txt", cmake) || !WriteIfMissing(scripts / "SDK" / "MetaCoreScripting" / "MetaCoreScriptSdk.h", sdk) || !WriteIfMissing(scripts / "Source" / "Module.cpp", module) || !WriteIfMissing(root / ".vscode" / "launch.json", launch)) { if (error) *error = "Failed to write script project files"; return false; } if (std::filesystem::exists(scripts / "Source" / "Mover.cpp")) { return true; } return MetaCoreCreateScriptBehaviour(root, projectName, "Mover", error); } bool MetaCoreCreateScriptBehaviour(const std::filesystem::path& root, std::string_view projectName, std::string_view behaviourName, std::string* error) { const std::string moduleId = SafeIdentifier(projectName); const std::string name = SafeIdentifier(behaviourName); if (name.empty() || name != behaviourName) { if (error) *error = "Behaviour name must contain only ASCII letters, digits, or underscores"; return false; } const auto path = root / "Scripts" / "Source" / (name + ".cpp"); if (std::filesystem::exists(path)) { if (error) *error = "Behaviour source already exists: " + path.string(); return false; } const std::string source = "#include \n#include \nstruct " + name + " {};\nstatic void* Create" + name + "() noexcept { return new (std::nothrow) " + name + "; }\nstatic void Destroy" + name + "(void* value) noexcept { delete static_cast<" + name + "*>(value); }\nstatic const char* Update" + name + "(void*, const MetaCoreScriptExecutionContextV1* context, double delta) noexcept { return MetaCoreScriptSdk::Guard([&] { MetaCoreScriptValueV1 speed{}; if(!context->Host->GetField(context->HostContext,context->Object,context->InstanceId,\"speed\",&speed)) return; MetaCoreScriptValueV1 position{}; if(!context->Host->GetProperty(context->HostContext,context->Object,\"Transform\",\"Position\",&position)) return; position.Vec3[0]+=speed.NumberValue*delta; context->Host->SetProperty(context->HostContext,context->Object,\"Transform\",\"Position\",&position); }); }\nstatic const MetaCoreScriptFieldDescriptorV1 " + name + "Fields[]={{\"speed\",\"Speed\",MetaCoreScriptAbiValue_Float,\"1.0\"}};\nstatic const MetaCoreScriptBehaviourDescriptorV1 " + name + "Descriptor={\"" + moduleId + "." + name + "\",\"" + name + "\",1,1," + name + "Fields,Create" + name + ",Destroy" + name + ",nullptr,nullptr,Update" + name + ",nullptr,nullptr};\nMC_REGISTER_SCRIPT(" + name + ", " + name + "Descriptor);\n"; if (!WriteIfMissing(path, source)) { if (error) *error = "Failed to create behaviour source: " + path.string(); return false; } return true; } MetaCoreScriptProjectBuildResult MetaCoreBuildScriptProject(const std::filesystem::path& root, std::string_view configuration, const std::filesystem::path&) { MetaCoreScriptProjectBuildResult result; const auto now = std::chrono::system_clock::now().time_since_epoch(); result.BuildId = std::to_string(std::chrono::duration_cast(now).count()); result.BuildDirectory = root / "Library" / "ScriptBuild" / std::string(configuration); const auto output = root / "Library" / "ScriptModules" / "Linux-x86_64" / std::string(configuration) / result.BuildId; std::error_code ec; std::filesystem::remove(result.BuildDirectory / "CMakeCache.txt", ec); ec.clear(); std::filesystem::remove_all(result.BuildDirectory / "CMakeFiles", ec); ec.clear(); std::filesystem::create_directories(result.BuildDirectory, ec); std::filesystem::create_directories(output, ec); const auto log = result.BuildDirectory / ("build-" + result.BuildId + ".log"); const std::string configure = "cmake -S " + ShellQuote(root / "Scripts") + " -B " + ShellQuote(result.BuildDirectory) + " -DCMAKE_CXX_COMPILER=clang++-15 -DCMAKE_CXX_FLAGS=-stdlib=libc++ -DCMAKE_SHARED_LINKER_FLAGS=-stdlib=libc++ -DCMAKE_BUILD_TYPE=" + std::string(configuration) + " -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DMETACORE_SCRIPT_BUILD_ID=" + result.BuildId + " -DMETACORE_SCRIPT_OUTPUT_DIR=" + ShellQuote(output) + " > " + ShellQuote(log) + " 2>&1"; int code = std::system(configure.c_str()); if (code == 0) { const std::string build = "cmake --build " + ShellQuote(result.BuildDirectory) + " --config " + std::string(configuration) + " >> " + ShellQuote(log) + " 2>&1"; code = std::system(build.c_str()); } std::ifstream input(log); std::ostringstream text; text << input.rdbuf(); result.Output = text.str(); if (code == 0) { for (const auto& entry : std::filesystem::directory_iterator(output, ec)) if (entry.path().extension() == ".so") { result.ModulePath = entry.path(); break; } } result.Success = code == 0 && !result.ModulePath.empty(); if (result.Success) { std::filesystem::copy_file(result.BuildDirectory / "compile_commands.json", root / "compile_commands.json", std::filesystem::copy_options::overwrite_existing, ec); std::string loadError; MetaCoreScriptModuleLoader loader; const auto loaded = loader.LoadCandidate(result.ModulePath, &loadError); const auto hash = MetaCoreSha256File(result.ModulePath); if (!loaded.has_value() || !hash.has_value()) { result.Success = false; result.Output += "\nPost-build module validation failed: " + loadError; } else { result.ManifestPath = result.ModulePath; result.ManifestPath += ".mcscriptmodule.json"; std::ofstream manifest(result.ManifestPath, std::ios::trunc); manifest << Json{{"format_version", 1}, {"abi_major", METACORE_SCRIPT_ABI_MAJOR}, {"abi_minor", METACORE_SCRIPT_ABI_MINOR}, {"module_id", loaded->ModuleId}, {"build_id", result.BuildId}, {"module_file", result.ModulePath.filename().string()}, {"sha256", *hash}}.dump(2) << '\n'; result.Success = manifest.good(); } } return result; } std::optional MetaCoreFindLatestScriptModule(const std::filesystem::path& root, std::string_view configuration) { const auto base = root / "Library" / "ScriptModules" / "Linux-x86_64" / std::string(configuration); if (!std::filesystem::is_directory(base)) return std::nullopt; std::filesystem::path latest; std::filesystem::file_time_type time{}; std::error_code ec; for (const auto& entry : std::filesystem::recursive_directory_iterator(base, ec)) if (entry.path().extension() == ".so") { auto t = entry.last_write_time(ec); if (latest.empty() || t > time) { latest = entry.path(); time = t; } } return latest.empty() ? std::nullopt : std::optional(latest); } } // namespace MetaCore