From 0e69178be8da8c4de49c2e6e2b21a088b4f7a9f1 Mon Sep 17 00:00:00 2001 From: M1KUS3Q Date: Sat, 18 Jul 2026 15:24:11 +0200 Subject: [PATCH 1/7] feat: add event system primitives (EventListener, Delegate, MarkerEventLink) --- .gitignore | 1 + include/events/Delegate.hpp | 38 +++++++++++++++++++ include/events/EventListener.hpp | 19 ++++++++++ include/events/MarkerEventLink.hpp | 24 ++++++++++++ .../components/MarkerEmitterComponent.hpp | 18 +++++++++ 5 files changed, 100 insertions(+) create mode 100644 include/events/Delegate.hpp create mode 100644 include/events/EventListener.hpp create mode 100644 include/events/MarkerEventLink.hpp create mode 100644 include/scene/components/MarkerEmitterComponent.hpp diff --git a/.gitignore b/.gitignore index d1b4e06..9017fac 100644 --- a/.gitignore +++ b/.gitignore @@ -45,6 +45,7 @@ CTestTestfile.cmake _deps/ # IDEs and Editors +.clangd .vscode/ .idea/ *.swp diff --git a/include/events/Delegate.hpp b/include/events/Delegate.hpp new file mode 100644 index 0000000..cc2ae12 --- /dev/null +++ b/include/events/Delegate.hpp @@ -0,0 +1,38 @@ +#ifndef DELEGATE_HPP +#define DELEGATE_HPP + +#include +#include + +#include "events/EventListener.hpp" + +struct Context; + +class Delegate { + public: + Delegate() = default; + ~Delegate() = default; + + Delegate(const Delegate&) = delete; + Delegate& operator=(const Delegate&) = delete; + Delegate(Delegate&&) = delete; + Delegate& operator=(Delegate&&) = delete; + + void subscribe(EventListener* listener) { listeners.push_back(listener); } + + void unsubscribe(EventListener* listener) { + listeners.erase(std::remove(listeners.begin(), listeners.end(), listener), listeners.end()); + } + + void invoke(const Context& ctx) { + for (auto* listener : listeners) { + listener->onEventTriggered(ctx); + } + } + + private: + // NOLINTNEXTLINE(cppcoreguidelines-non-private-member-variables-in-classes) + std::vector listeners; +}; + +#endif // DELEGATE_HPP diff --git a/include/events/EventListener.hpp b/include/events/EventListener.hpp new file mode 100644 index 0000000..787e79e --- /dev/null +++ b/include/events/EventListener.hpp @@ -0,0 +1,19 @@ +#ifndef EVENTLISTENER_HPP +#define EVENTLISTENER_HPP + +struct Context; + +class EventListener { + public: + EventListener() = default; + virtual ~EventListener() = default; + + EventListener(const EventListener&) = delete; + EventListener& operator=(const EventListener&) = delete; + EventListener(EventListener&&) = delete; + EventListener& operator=(EventListener&&) = delete; + + virtual void onEventTriggered(const Context& ctx) = 0; +}; + +#endif // EVENTLISTENER_HPP diff --git a/include/events/MarkerEventLink.hpp b/include/events/MarkerEventLink.hpp new file mode 100644 index 0000000..69cff36 --- /dev/null +++ b/include/events/MarkerEventLink.hpp @@ -0,0 +1,24 @@ +#ifndef MARKEREVENTLINK_HPP +#define MARKEREVENTLINK_HPP + +#include +#include + +#include "data_structures/Context.hpp" +#include "events/EventListener.hpp" + +class MarkerEventLink : public EventListener { + public: + explicit MarkerEventLink(std::string name) : eventName(std::move(name)) {} + + void onEventTriggered(const Context& ctx) override { + if (ctx.markers != nullptr) { + ctx.markers->push_back(eventName); + } + } + + private: + std::string eventName; +}; + +#endif // MARKEREVENTLINK_HPP diff --git a/include/scene/components/MarkerEmitterComponent.hpp b/include/scene/components/MarkerEmitterComponent.hpp new file mode 100644 index 0000000..b27176f --- /dev/null +++ b/include/scene/components/MarkerEmitterComponent.hpp @@ -0,0 +1,18 @@ +#ifndef MARKEREMITTERCOMPONENT_HPP +#define MARKEREMITTERCOMPONENT_HPP + +#include + +#include "Component.hpp" + +namespace NeuronIDE { +class Component; +} + +class MarkerEmitterComponent : public Component { + public: + void update(const Context& context) override; + void render(SDL_Renderer* renderer) override; +}; + +#endif // BLINKCOMPONENT_HPP \ No newline at end of file From a70d52b6414787dbcc4c195f91fc7d85dd7e141f Mon Sep 17 00:00:00 2001 From: M1KUS3Q Date: Sat, 18 Jul 2026 15:25:51 +0200 Subject: [PATCH 2/7] feat: add onSceneReady lifecycle and getEvent/resolveEvents to Component and Scene --- include/scene/Scene.hpp | 5 +++++ include/scene/components/Component.hpp | 9 ++++++++ src/scene/Scene.cpp | 30 ++++++++++++++++++++++++++ 3 files changed, 44 insertions(+) diff --git a/include/scene/Scene.hpp b/include/scene/Scene.hpp index d0c1025..163ad76 100644 --- a/include/scene/Scene.hpp +++ b/include/scene/Scene.hpp @@ -3,8 +3,10 @@ #include #include +#include #include +class Delegate; class SceneObject; class SDL_Renderer; struct Context; @@ -24,6 +26,9 @@ class Scene { void update(const Context& ctx); void render(SDL_Renderer* renderer); + + void onSceneReady(); + Delegate& resolveEvents(std::string_view objectName, std::string_view eventName); }; #endif // SCENE_HPP \ No newline at end of file diff --git a/include/scene/components/Component.hpp b/include/scene/components/Component.hpp index 4e8d467..efb67d1 100644 --- a/include/scene/components/Component.hpp +++ b/include/scene/components/Component.hpp @@ -2,7 +2,10 @@ #define COMPONENT_HPP #include +#include +class Delegate; +class Scene; class SceneObject; class SDL_Renderer; struct Context; @@ -21,6 +24,12 @@ class Component { virtual void update(const Context& context) = 0; virtual void render(SDL_Renderer* renderer) = 0; + virtual void onSceneReady(Scene& scene) { (void)scene; } + virtual Delegate* getEvent(std::string_view eventName) { + (void)eventName; + return nullptr; + } + protected: // NOLINTNEXTLINE(cppcoreguidelines-non-private-member-variables-in-classes) std::weak_ptr owner; diff --git a/src/scene/Scene.cpp b/src/scene/Scene.cpp index 9360b5c..b6a67e6 100644 --- a/src/scene/Scene.cpp +++ b/src/scene/Scene.cpp @@ -1,6 +1,10 @@ #include "scene/Scene.hpp" +#include + +#include "events/Delegate.hpp" #include "scene/SceneObject.hpp" +#include "scene/components/Component.hpp" void Scene::update(const Context& ctx) { for (const auto& obj : objects) { @@ -14,4 +18,30 @@ void Scene::render(SDL_Renderer* renderer) { obj->render(renderer); } } +} + +void Scene::onSceneReady() { + for (const auto& obj : objects) { + for (const auto& comp : obj->components) { + comp->onSceneReady(*this); + } + } +} + +Delegate& Scene::resolveEvents(std::string_view objectName, std::string_view eventName) { + for (const auto& obj : objects) { + if (obj->name != objectName) { + continue; + } + for (const auto& comp : obj->components) { + auto* delegate = comp->getEvent(eventName); + if (delegate != nullptr) { + return *delegate; + } + } + throw std::runtime_error("Scene::resolveEvents: event '" + std::string(eventName) + + "' not found on object '" + std::string(objectName) + "'"); + } + throw std::runtime_error("Scene::resolveEvents: object '" + std::string(objectName) + + "' not found"); } \ No newline at end of file From 99be3bf3914d456678b3cff928d5c899db3d0c54 Mon Sep 17 00:00:00 2001 From: M1KUS3Q Date: Sat, 18 Jul 2026 15:26:54 +0200 Subject: [PATCH 3/7] feat: trigger onSceneReady lifecycle from Parser --- src/parser/Parser.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/parser/Parser.cpp b/src/parser/Parser.cpp index f651cc9..43413f6 100644 --- a/src/parser/Parser.cpp +++ b/src/parser/Parser.cpp @@ -43,6 +43,8 @@ std::shared_ptr Parser::parseStream(std::istream& stream) { scene->addObject(std::move(obj)); } + scene->onSceneReady(); + return scene; } From d221afbbada7223361bb20d625a449dd5ff12e77 Mon Sep 17 00:00:00 2001 From: M1KUS3Q Date: Sat, 18 Jul 2026 15:28:06 +0200 Subject: [PATCH 4/7] feat: add MarkerEmitterComponent protobuf message and extend Component oneof --- protoFiles/neuronide.proto | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/protoFiles/neuronide.proto b/protoFiles/neuronide.proto index 24e15a3..0623fb6 100644 --- a/protoFiles/neuronide.proto +++ b/protoFiles/neuronide.proto @@ -21,6 +21,15 @@ message ScriptComponent { string script_path = 1; } +message MarkerEmitterComponent { + message Binding { + string target_object = 1; + string target_event = 2; + string marker_name = 3; + } + repeated Binding bindings = 1; +} + message Transform { double x = 1; double y = 2; @@ -35,6 +44,7 @@ message Component { TextRenderer text = 2; BlinkComponent blinker = 3; ScriptComponent script = 4; + MarkerEmitterComponent marker_emitter = 5; } } From 2a00cc4c7d9044be6baafdadb3addf94e879ff2f Mon Sep 17 00:00:00 2001 From: M1KUS3Q Date: Sat, 18 Jul 2026 15:30:01 +0200 Subject: [PATCH 5/7] feat: implement MarkerEmitterComponent --- .../components/MarkerEmitterComponent.hpp | 41 +++++++++++++++-- src/scene/CMakeLists.txt | 1 + .../components/MarkerEmitterComponent.cpp | 44 +++++++++++++++++++ 3 files changed, 83 insertions(+), 3 deletions(-) create mode 100644 src/scene/components/MarkerEmitterComponent.cpp diff --git a/include/scene/components/MarkerEmitterComponent.hpp b/include/scene/components/MarkerEmitterComponent.hpp index b27176f..dbb9f75 100644 --- a/include/scene/components/MarkerEmitterComponent.hpp +++ b/include/scene/components/MarkerEmitterComponent.hpp @@ -1,9 +1,15 @@ #ifndef MARKEREMITTERCOMPONENT_HPP #define MARKEREMITTERCOMPONENT_HPP -#include +#include +#include +#include -#include "Component.hpp" +#include "events/MarkerEventLink.hpp" +#include "scene/components/Component.hpp" + +class Delegate; +class Scene; namespace NeuronIDE { class Component; @@ -11,8 +17,37 @@ class Component; class MarkerEmitterComponent : public Component { public: + struct Binding { + std::string targetObject; + std::string targetEvent; + std::string markerName; + }; + + MarkerEmitterComponent(const std::shared_ptr& owner, + std::vector bindings); + ~MarkerEmitterComponent() override; + + MarkerEmitterComponent(const MarkerEmitterComponent&) = delete; + MarkerEmitterComponent& operator=(const MarkerEmitterComponent&) = delete; + MarkerEmitterComponent(MarkerEmitterComponent&&) = delete; + MarkerEmitterComponent& operator=(MarkerEmitterComponent&&) = delete; + void update(const Context& context) override; void render(SDL_Renderer* renderer) override; + void onSceneReady(Scene& scene) override; + + static std::unique_ptr createMarkerEmitter( + const NeuronIDE::Component& protoComp, + const std::shared_ptr& owner); + + private: + struct ActiveSubscription { + std::unique_ptr link; + Delegate* delegate = nullptr; + }; + + std::vector bindings; + std::vector activeSubscriptions; }; -#endif // BLINKCOMPONENT_HPP \ No newline at end of file +#endif // MARKEREMITTERCOMPONENT_HPP diff --git a/src/scene/CMakeLists.txt b/src/scene/CMakeLists.txt index 1338a40..212cb07 100644 --- a/src/scene/CMakeLists.txt +++ b/src/scene/CMakeLists.txt @@ -3,6 +3,7 @@ add_library(scene OBJECT Scene.cpp components/ComponentRegistry.cpp components/BlinkComponent.cpp + components/MarkerEmitterComponent.cpp ) target_include_directories(scene PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/../../include) target_link_libraries(scene PUBLIC neuronide_proto) \ No newline at end of file diff --git a/src/scene/components/MarkerEmitterComponent.cpp b/src/scene/components/MarkerEmitterComponent.cpp new file mode 100644 index 0000000..7501485 --- /dev/null +++ b/src/scene/components/MarkerEmitterComponent.cpp @@ -0,0 +1,44 @@ +#include "scene/components/MarkerEmitterComponent.hpp" + +#include "events/Delegate.hpp" +#include "neuronide.pb.h" +#include "scene/Scene.hpp" +#include "scene/components/ComponentRegistry.hpp" + +MarkerEmitterComponent::MarkerEmitterComponent(const std::shared_ptr& owner, + std::vector bindings) + : Component(owner), bindings(std::move(bindings)) {} + +MarkerEmitterComponent::~MarkerEmitterComponent() { + for (auto& sub : activeSubscriptions) { + sub.delegate->unsubscribe(sub.link.get()); + } +} + +void MarkerEmitterComponent::update(const Context& context) { (void)context; } + +void MarkerEmitterComponent::render(SDL_Renderer* renderer) { (void)renderer; } + +void MarkerEmitterComponent::onSceneReady(Scene& scene) { + for (const auto& binding : bindings) { + auto& delegate = scene.resolveEvents(binding.targetObject, binding.targetEvent); + + auto link = std::make_unique(binding.markerName); + delegate.subscribe(link.get()); + + activeSubscriptions.push_back({std::move(link), &delegate}); + } +} + +std::unique_ptr MarkerEmitterComponent::createMarkerEmitter( + const NeuronIDE::Component& protoComp, const std::shared_ptr& owner) { + std::vector bindings; + for (const auto& protoBinding : protoComp.marker_emitter().bindings()) { + bindings.push_back({protoBinding.target_object(), protoBinding.target_event(), + protoBinding.marker_name()}); + } + return std::make_unique(owner, std::move(bindings)); +} + +REGISTER_COMPONENT(NeuronIDE::Component::kMarkerEmitter, + MarkerEmitterComponent::createMarkerEmitter) From 926523ae2842d4710d49aa3aea4e243e66d5c8dd Mon Sep 17 00:00:00 2001 From: M1KUS3Q Date: Sat, 18 Jul 2026 15:33:01 +0200 Subject: [PATCH 6/7] test: add unit tests for event system and MarkerEmitterComponent --- tests/component_tests/marker_emitter_test.cpp | 177 ++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 tests/component_tests/marker_emitter_test.cpp diff --git a/tests/component_tests/marker_emitter_test.cpp b/tests/component_tests/marker_emitter_test.cpp new file mode 100644 index 0000000..31e7120 --- /dev/null +++ b/tests/component_tests/marker_emitter_test.cpp @@ -0,0 +1,177 @@ +#include + +#include +#include +#include +#include + +#include "data_structures/Context.hpp" +#include "events/Delegate.hpp" +#include "events/MarkerEventLink.hpp" +#include "scene/Scene.hpp" +#include "scene/SceneObject.hpp" +#include "scene/components/Component.hpp" +#include "scene/components/MarkerEmitterComponent.hpp" + +// ------------------------------------------------------------------- +// Mock event listener for delegate tests +// ------------------------------------------------------------------- +class MockListener : public EventListener { + public: + void onEventTriggered(const Context& ctx) override { + (void)ctx; + callCount++; + } + + int callCount = 0; +}; + +// ------------------------------------------------------------------- +// Test component that exposes a Delegate via getEvent() +// ------------------------------------------------------------------- +class TestEventProvider : public Component { + public: + explicit TestEventProvider(const std::shared_ptr& owner) : Component(owner) {} + + Delegate* getEvent(std::string_view eventName) override { + if (eventName == "trigger") { + return &testDelegate; + } + return nullptr; + } + + void update(const Context& /*ctx*/) override {} + void render(SDL_Renderer* /*renderer*/) override {} + + Delegate testDelegate; +}; + +// =================================================================== +// Delegate tests +// =================================================================== +TEST(DelegateTest, SubscribeInvokesListener) { + Delegate delegate; + MockListener listener; + + delegate.subscribe(&listener); + std::vector markers; + Context ctx{0.0, &markers}; + delegate.invoke(ctx); + + EXPECT_EQ(listener.callCount, 1); +} + +TEST(DelegateTest, UnsubscribeRemovesListener) { + Delegate delegate; + MockListener listener; + + delegate.subscribe(&listener); + delegate.unsubscribe(&listener); + std::vector markers; + Context ctx{0.0, &markers}; + delegate.invoke(ctx); + + EXPECT_EQ(listener.callCount, 0); +} + +TEST(DelegateTest, MultipleListenersAllCalled) { + Delegate delegate; + MockListener listener1; + MockListener listener2; + MockListener listener3; + + delegate.subscribe(&listener1); + delegate.subscribe(&listener2); + delegate.subscribe(&listener3); + std::vector markers; + Context ctx{0.0, &markers}; + delegate.invoke(ctx); + + EXPECT_EQ(listener1.callCount, 1); + EXPECT_EQ(listener2.callCount, 1); + EXPECT_EQ(listener3.callCount, 1); +} + +// =================================================================== +// MarkerEventLink tests +// =================================================================== +TEST(MarkerEventLinkTest, PushesMarkerToContext) { + MarkerEventLink link("test_event"); + std::vector markers; + Context ctx{0.0, &markers}; + + link.onEventTriggered(ctx); + + ASSERT_EQ(markers.size(), 1); + EXPECT_EQ(markers[0], "test_event"); +} + +TEST(MarkerEventLinkTest, NullMarkersNoCrash) { + MarkerEventLink link("test_event"); + Context ctx{0.0, nullptr}; + + EXPECT_NO_THROW(link.onEventTriggered(ctx)); +} + +// =================================================================== +// Scene::resolveEvents tests +// =================================================================== +TEST(ResolveEventsTest, ReturnsDelegateFromComponent) { + auto scene = std::make_shared(); + auto obj = std::make_shared("Provider"); + auto comp = std::make_unique(obj); + obj->addComponent(std::move(comp)); + scene->addObject(obj); + + auto& delegate = scene->resolveEvents("Provider", "trigger"); + // Should return a valid reference + EXPECT_NO_THROW(delegate.invoke(Context{})); +} + +TEST(ResolveEventsTest, ThrowsOnMissingObject) { + auto scene = std::make_shared(); + + EXPECT_THROW(scene->resolveEvents("NoSuchObject", "trigger"), std::runtime_error); +} + +TEST(ResolveEventsTest, ThrowsOnMissingEvent) { + auto scene = std::make_shared(); + auto obj = std::make_shared("Provider"); + auto comp = std::make_unique(obj); + obj->addComponent(std::move(comp)); + scene->addObject(obj); + + EXPECT_THROW(scene->resolveEvents("Provider", "no_such_event"), std::runtime_error); +} + +// =================================================================== +// MarkerEmitterComponent integration test +// =================================================================== +TEST(MarkerEmitterIntegration, FullFlow) { + // Build scene with a provider and an emitter + auto scene = std::make_shared(); + + auto providerObj = std::make_shared("Provider"); + auto providerComp = std::make_unique(providerObj); + auto* providerPtr = providerComp.get(); + providerObj->addComponent(std::move(providerComp)); + scene->addObject(providerObj); + + auto emitterObj = std::make_shared("Emitter"); + std::vector bindings = { + {"Provider", "trigger", "hello_world"}}; + emitterObj->addComponent( + std::make_unique(emitterObj, std::move(bindings))); + scene->addObject(emitterObj); + + // Simulate onSceneReady + scene->onSceneReady(); + + // Fire the delegate + std::vector markers; + Context ctx{0.0, &markers}; + providerPtr->testDelegate.invoke(ctx); + + ASSERT_EQ(markers.size(), 1); + EXPECT_EQ(markers[0], "hello_world"); +} From 0a371fcc7777f0b60f562095c2a3e8ec6111ba70 Mon Sep 17 00:00:00 2001 From: M1KUS3Q Date: Sat, 18 Jul 2026 15:48:20 +0200 Subject: [PATCH 7/7] chore: format via clang-format --- include/events/EventListener.hpp | 2 +- include/scene/Scene.hpp | 2 +- include/scene/components/Component.hpp | 2 +- include/scene/components/MarkerEmitterComponent.hpp | 7 +++---- tests/component_tests/marker_emitter_test.cpp | 6 +++--- 5 files changed, 9 insertions(+), 10 deletions(-) diff --git a/include/events/EventListener.hpp b/include/events/EventListener.hpp index 787e79e..b2558b1 100644 --- a/include/events/EventListener.hpp +++ b/include/events/EventListener.hpp @@ -5,7 +5,7 @@ struct Context; class EventListener { public: - EventListener() = default; + EventListener() = default; virtual ~EventListener() = default; EventListener(const EventListener&) = delete; diff --git a/include/scene/Scene.hpp b/include/scene/Scene.hpp index 163ad76..358f526 100644 --- a/include/scene/Scene.hpp +++ b/include/scene/Scene.hpp @@ -27,7 +27,7 @@ class Scene { void update(const Context& ctx); void render(SDL_Renderer* renderer); - void onSceneReady(); + void onSceneReady(); Delegate& resolveEvents(std::string_view objectName, std::string_view eventName); }; diff --git a/include/scene/components/Component.hpp b/include/scene/components/Component.hpp index efb67d1..0f17992 100644 --- a/include/scene/components/Component.hpp +++ b/include/scene/components/Component.hpp @@ -24,7 +24,7 @@ class Component { virtual void update(const Context& context) = 0; virtual void render(SDL_Renderer* renderer) = 0; - virtual void onSceneReady(Scene& scene) { (void)scene; } + virtual void onSceneReady(Scene& scene) { (void)scene; } virtual Delegate* getEvent(std::string_view eventName) { (void)eventName; return nullptr; diff --git a/include/scene/components/MarkerEmitterComponent.hpp b/include/scene/components/MarkerEmitterComponent.hpp index dbb9f75..3a14ad1 100644 --- a/include/scene/components/MarkerEmitterComponent.hpp +++ b/include/scene/components/MarkerEmitterComponent.hpp @@ -37,8 +37,7 @@ class MarkerEmitterComponent : public Component { void onSceneReady(Scene& scene) override; static std::unique_ptr createMarkerEmitter( - const NeuronIDE::Component& protoComp, - const std::shared_ptr& owner); + const NeuronIDE::Component& protoComp, const std::shared_ptr& owner); private: struct ActiveSubscription { @@ -46,8 +45,8 @@ class MarkerEmitterComponent : public Component { Delegate* delegate = nullptr; }; - std::vector bindings; - std::vector activeSubscriptions; + std::vector bindings; + std::vector activeSubscriptions; }; #endif // MARKEREMITTERCOMPONENT_HPP diff --git a/tests/component_tests/marker_emitter_test.cpp b/tests/component_tests/marker_emitter_test.cpp index 31e7120..dbd486c 100644 --- a/tests/component_tests/marker_emitter_test.cpp +++ b/tests/component_tests/marker_emitter_test.cpp @@ -151,9 +151,9 @@ TEST(MarkerEmitterIntegration, FullFlow) { // Build scene with a provider and an emitter auto scene = std::make_shared(); - auto providerObj = std::make_shared("Provider"); - auto providerComp = std::make_unique(providerObj); - auto* providerPtr = providerComp.get(); + auto providerObj = std::make_shared("Provider"); + auto providerComp = std::make_unique(providerObj); + auto* providerPtr = providerComp.get(); providerObj->addComponent(std::move(providerComp)); scene->addObject(providerObj);